Skip to content

Commit 70fb8ae

Browse files
authored
Merge pull request #166 from canonical/create-sample-data
Add a script to generate sample data for a dev/test environment
2 parents 78c1abc + 82ed23e commit 70fb8ae

File tree

3 files changed

+275
-0
lines changed

3 files changed

+275
-0
lines changed

server/HACKING.md

+25
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ this running on your system:
2424
$ docker-compose up -d
2525
```
2626

27+
If you want to add some sample data to your local dev environment created
28+
using the commands above, there's a helper script for this.
29+
```
30+
$ devel/create_sample_data.py -h
31+
32+
usage: create_sample_data.py [-h] [-a AGENTS] [-j JOBS] [-q QUEUES] [-s SERVER]
33+
34+
Create sample data for testing Testflinger
35+
36+
options:
37+
-h, --help show this help message and exit
38+
-a AGENTS, --agents AGENTS
39+
Number of agents to create
40+
-j JOBS, --jobs JOBS Number of jobs to create
41+
-q QUEUES, --queues QUEUES
42+
Number of queues to create
43+
-s SERVER, --server SERVER
44+
URL of testflinger server starting with 'http(s)://...' (must not be production server)
45+
```
46+
47+
The defaults are intended to be used with a server running on
48+
http://localhost:5000 which is what will be deployed by default if you use
49+
the docker-compose setup above. So if this is what you want, you can just
50+
call it with no options.
51+
2752

2853
## Multipass
2954

server/devel/create_sample_data.py

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
#!/usr/bin/env python3
2+
# Copyright (C) 2023 Canonical
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License.
7+
#
8+
# This program is distributed in the hope that it will be useful,
9+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
# GNU General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
"""
16+
Generate sample data for use in local testing and development.
17+
This will send the data to the Testflinger server specified, but will not
18+
allow you to use the production server.
19+
"""
20+
21+
import logging
22+
import random
23+
import sys
24+
from argparse import ArgumentParser, Namespace
25+
from typing import Iterator, Optional, Tuple
26+
27+
import requests
28+
29+
logging.basicConfig(level=logging.INFO)
30+
31+
32+
def get_args() -> Namespace:
33+
"""Parse command line arguments
34+
:return: Namespace containing parsed arguments
35+
"""
36+
default_testflinger_server = "http://localhost:5000"
37+
parser = ArgumentParser(
38+
description="Create sample data for testing Testflinger"
39+
)
40+
41+
def _server_validator(server: str) -> str:
42+
if not server.startswith("http"):
43+
raise ValueError("Server must start with http")
44+
if "testflinger.canonical.com" in server:
45+
raise ValueError("Cannot use production server")
46+
return server
47+
48+
parser.add_argument(
49+
"-a",
50+
"--agents",
51+
type=int,
52+
default=10,
53+
help="Number of agents to create",
54+
)
55+
56+
parser.add_argument(
57+
"-j", "--jobs", type=int, default=10, help="Number of jobs to create"
58+
)
59+
60+
parser.add_argument(
61+
"-q",
62+
"--queues",
63+
type=int,
64+
default=10,
65+
help="Number of queues to create",
66+
)
67+
68+
parser.add_argument(
69+
"-s",
70+
"--server",
71+
default=default_testflinger_server,
72+
type=_server_validator,
73+
help=(
74+
"URL of testflinger server starting with 'http(s)://...' "
75+
"(must not be production server)"
76+
),
77+
)
78+
return parser.parse_args()
79+
80+
81+
class AgentDataGenerator: # pylint: disable=too-few-public-methods
82+
"""Agent data generator"""
83+
84+
def __init__(
85+
self,
86+
prefix: str = "agent",
87+
num_agents: int = 10,
88+
queue_list: Optional[Tuple[str, ...]] = None,
89+
):
90+
"""Generate sample agent data
91+
:param prefix: Prefix for the agent name
92+
:param num_agents: Number of agents to generate
93+
:param queue_list: Tuple of queues to assign to agents
94+
:return: List of dictionaries containing agent data
95+
self.data_list = []
96+
for agent_num in range(num_agents):
97+
agent_data: dict = {
98+
"state": random.choice(("waiting", "test", "provision")),
99+
}
100+
if queue_list:
101+
agent_data["queues"] = [random.choice(queue_list)]
102+
self.data_list.append({f"{prefix}{agent_num}": agent_data})
103+
"""
104+
self.prefix = prefix
105+
self.num_agents = num_agents
106+
self.queue_list = queue_list
107+
108+
def __iter__(self):
109+
for agent_num in range(self.num_agents):
110+
agent_data = {
111+
"state": random.choice(("waiting", "test", "provision")),
112+
}
113+
if self.queue_list:
114+
agent_data["queues"] = [random.choice(self.queue_list)]
115+
yield (f"{self.prefix}{agent_num}", agent_data)
116+
117+
118+
class JobDataGenerator: # pylint: disable=too-few-public-methods
119+
"""Job data generator"""
120+
121+
def __init__(
122+
self,
123+
prefix: str = "job",
124+
num_jobs: int = 10,
125+
queue_list: Optional[Tuple[str, ...]] = None,
126+
):
127+
"""Generate sample job data
128+
:param prefix: Prefix for the job name
129+
:param num_jobs: Number of jobs to generate
130+
:param queue_list: Tuple of queues to assign to jobs
131+
:return: List of dictionaries containing job data
132+
"""
133+
self.prefix = prefix
134+
self.num_jobs = num_jobs
135+
self.queue_list = queue_list
136+
137+
def __iter__(self):
138+
for _ in range(self.num_jobs):
139+
yield {
140+
"job_queue": random.choice(self.queue_list),
141+
"test_data": {"test_cmds": "echo test"},
142+
}
143+
144+
145+
class QueueDataGenerator: # pylint: disable=too-few-public-methods
146+
"""Queue data generator"""
147+
148+
def __init__(
149+
self,
150+
prefix: str = "test_queue",
151+
description: str = "Example queue",
152+
num_queues: int = 10,
153+
):
154+
"""Generate sample queue data
155+
:param prefix: Prefix for the queue name
156+
:param description: Description for the queue
157+
:param num_queues: Number of queues to generate
158+
:return: List of dictionaries containing queue data
159+
"""
160+
self.prefix = prefix
161+
self.description = description
162+
self.num_queues = num_queues
163+
164+
def __iter__(self):
165+
for queue_num in range(self.num_queues):
166+
yield {
167+
f"{self.prefix}{queue_num}": f"{self.description} {queue_num}"
168+
}
169+
170+
171+
class TestflingerClient:
172+
"""Client to connect to Testflinger and post data"""
173+
174+
def __init__(self, server_url: str):
175+
self.server_url = server_url
176+
self.session = requests.Session()
177+
self.session.headers.update({"Content-Type": "application/json"})
178+
self.session.timeout = 3
179+
180+
def post_queue_data(self, queues: Iterator):
181+
"""Post queue data to Testflinger server
182+
:param queues: Iterator of queue data to post
183+
"""
184+
for queue in queues:
185+
self.session.post(
186+
f"{self.server_url}/v1/agents/queues",
187+
json=queue,
188+
)
189+
190+
def post_agent_data(self, agents: Iterator):
191+
"""Post agent data to Testflinger server
192+
:param agents: Iterator of agent data to post
193+
"""
194+
for agent_name, agent_data in agents:
195+
self.session.post(
196+
f"{self.server_url}/v1/agents/data/{agent_name}",
197+
json=agent_data,
198+
)
199+
200+
def post_job_data(self, jobs: Iterator):
201+
"""Post job data to Testflinger server
202+
:param jobs: Iterator of job data to post
203+
"""
204+
for job in jobs:
205+
self.session.post(
206+
f"{self.server_url}/v1/job",
207+
json=job,
208+
)
209+
210+
211+
def extract_queue_names(queues: Iterator) -> Tuple[str, ...]:
212+
"""Extract queue names from queue data
213+
:param queues: Iterator of queue data
214+
:return: Tuple of queue names
215+
"""
216+
return tuple(
217+
queue_name for queue_entry in queues for queue_name in queue_entry
218+
)
219+
220+
221+
def main():
222+
"""Main function"""
223+
args = get_args()
224+
225+
testflinger_client = TestflingerClient(server_url=args.server)
226+
227+
queues = QueueDataGenerator(num_queues=args.queues)
228+
testflinger_client.post_queue_data(queues=queues)
229+
logging.info("Created %s queues", args.queues)
230+
231+
valid_queue_names = extract_queue_names(queues=queues)
232+
233+
agents = AgentDataGenerator(
234+
num_agents=args.agents, queue_list=valid_queue_names
235+
)
236+
testflinger_client.post_agent_data(agents=agents)
237+
logging.info("Created %s agents", args.agents)
238+
239+
jobs = JobDataGenerator(num_jobs=args.jobs, queue_list=valid_queue_names)
240+
testflinger_client.post_job_data(jobs=jobs)
241+
logging.info("Created %s jobs", args.jobs)
242+
243+
244+
if __name__ == "__main__":
245+
try:
246+
main()
247+
except requests.exceptions.RequestException as error:
248+
logging.error(error)
249+
sys.exit(1)

server/tox.ini

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ deps =
1616
pytest
1717
pytest-mock
1818
pytest-cov
19+
requests
1920
# cosl needed for unit tests to pass
2021
cosl
2122
-r charm/requirements.txt

0 commit comments

Comments
 (0)