Merge ~pwlars/testflinger-cli:cli-reserve into testflinger-cli:master

Proposed by Paul Larson
Status: Merged
Approved by: Paul Larson
Approved revision: 0b9aab73440282e143b3b6ccb82d55aec6a33a03
Merged at revision: f2c57710ac2bd7addb3d04c345a37b36ebb5e346
Proposed branch: ~pwlars/testflinger-cli:cli-reserve
Merge into: testflinger-cli:master
Diff against target: 177 lines (+120/-9)
2 files modified
testflinger_cli/__init__.py (+110/-8)
testflinger_cli/client.py (+10/-1)
Reviewer Review Type Date Requested Status
Maciej Kisielewski (community) Approve
Sheila Miguez (community) Approve
Review via email: mp+377476@code.launchpad.net

Description of the change

This adds the cli pieces for a 'testflinger reserve' command. This can take args for queue, image, ssh_key, but I expect it will more commonly be used in interactive mode.

In interactive mode, it will guide the user through selecting the basic pieces needed to construct a yaml file, construct it for them, show them what it looks like, and even submit it. It also allows them to query the known queues and images while in this interactive mode.

I've tested this locally

To post a comment you must log in.
Revision history for this message
Sheila Miguez (codersquid) wrote :

lgtm

review: Approve
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

very nice! +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/testflinger_cli/__init__.py b/testflinger_cli/__init__.py
2index e93a1ce..440640a 100644
3--- a/testflinger_cli/__init__.py
4+++ b/testflinger_cli/__init__.py
5@@ -1,4 +1,4 @@
6-# Copyright (C) 2017-2019 Canonical
7+# Copyright (C) 2017-2020 Canonical
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11@@ -16,6 +16,7 @@
12
13
14 import click
15+import inspect
16 import json
17 import os
18 import sys
19@@ -111,6 +112,19 @@ def submit(ctx, filename, quiet, poll_opt):
20 raise SystemExit('File not found: {}'.format(filename))
21 except Exception:
22 raise SystemExit('Unable to read file: {}'.format(filename))
23+ job_id = submit_job_data(conn, data)
24+ if quiet:
25+ print(job_id)
26+ else:
27+ print('Job submitted successfully!')
28+ print('job_id: {}'.format(job_id))
29+ if poll_opt:
30+ ctx.invoke(poll, job_id=job_id)
31+
32+
33+def submit_job_data(conn, data):
34+ """ Submit data that was generated or read from a file as a test job
35+ """
36 try:
37 job_id = conn.submit_job(data)
38 except client.HTTPError as e:
39@@ -124,13 +138,7 @@ def submit(ctx, filename, quiet, poll_opt):
40 # This shouldn't happen, so let's get more information
41 raise SystemExit('Unexpected error status from testflinger '
42 'server: {}'.format(e.status))
43- if quiet:
44- print(job_id)
45- else:
46- print('Job submitted successfully!')
47- print('job_id: {}'.format(job_id))
48- if poll_opt:
49- ctx.invoke(poll, job_id=job_id)
50+ return job_id
51
52
53 @cli.command()
54@@ -272,6 +280,100 @@ def queues(ctx):
55 print(' {} - {}'.format(name, description))
56
57
58+@cli.command()
59+@click.option('--queue', '-q',
60+ help='Name of the queue to use')
61+@click.option('--image', '-i',
62+ help='Name of the image to use for provisioning')
63+@click.option('--key', '-k', 'ssh_keys',
64+ help='Ssh key to use for reservation (ex: lp:userid, gh:userid)')
65+@click.pass_context
66+def reserve(ctx, queue, image, ssh_keys):
67+ """Install and reserve a system"""
68+ conn = ctx.obj['conn']
69+ if not queue:
70+ try:
71+ queues = conn.get_queues()
72+ except Exception:
73+ print("WARNING: unable to get a list of queues from the server!")
74+ queues = {}
75+ queue = _get_queue(queues)
76+ if not image:
77+ try:
78+ images = conn.get_images(queue)
79+ except Exception:
80+ print("WARNING: unable to get a list of images from the server!")
81+ images = {}
82+ image = _get_image(images)
83+ if not ssh_keys:
84+ ssh_keys = _get_ssh_keys()
85+ template = inspect.cleandoc("""job_queue: {queue}
86+ provision_data:
87+ url: {image}
88+ reserve_data:
89+ ssh_keys:""")
90+ for ssh_key in ssh_keys:
91+ template += "\n - {}".format(ssh_key)
92+ job_data = template.format(queue=queue, image=image)
93+ print("\nThe following yaml will be submitted:")
94+ print(job_data)
95+ answer = input("Proceed? (Y/n) ")
96+ if answer in ("Y", "y", ""):
97+ job_id = submit_job_data(conn, job_data)
98+ print('Job submitted successfully!')
99+ print('job_id: {}'.format(job_id))
100+ ctx.invoke(poll, job_id=job_id)
101+
102+
103+def _get_queue(queues):
104+ queue = ""
105+ while not queue or queue == "?":
106+ queue = input("\nWhich queue do you want to use? ('?' to list) ")
107+ if not queue:
108+ continue
109+ if queue == "?":
110+ print("\nAdvertised queues on this server:")
111+ for name, description in queues.items():
112+ print(" {} - {}".format(name, description))
113+ queue = _get_queue(queues)
114+ if queue not in queues.keys():
115+ print("WARNING: '{}' is not in the list of known "
116+ "queues".format(queue))
117+ answer = input("Do you still want to use it? (y/N) ")
118+ if answer.lower() != "y":
119+ queue = ""
120+ return queue
121+
122+
123+def _get_image(images):
124+ image = ""
125+ while not image or image == "?":
126+ image = input("\nEnter the name of the image you want to use "
127+ "('?' to list) ")
128+ if image == "?":
129+ for image_id in images.keys():
130+ print(" " + image_id)
131+ continue
132+ if image not in images.keys():
133+ print("ERROR: '{}' is not in the list of known images for that "
134+ "queue, please select another.".format(image))
135+ image = ""
136+ return images.get(image)
137+
138+
139+def _get_ssh_keys():
140+ ssh_keys = ""
141+ while not ssh_keys.strip():
142+ ssh_keys = input("\nEnter the ssh key(s) you wish to use: "
143+ "(ex: lp:userid, gh:userid) ")
144+ key_list = [ssh_key.strip() for ssh_key in ssh_keys.split(",")]
145+ for ssh_key in key_list:
146+ if not ssh_key.startswith("lp:") and not ssh_key.startswith("gh:"):
147+ ssh_keys = ""
148+ print("Please enter keys in the form lp:userid or gh:userid")
149+ return key_list
150+
151+
152 def get_latest_output(conn, job_id):
153 output = ''
154 try:
155diff --git a/testflinger_cli/client.py b/testflinger_cli/client.py
156index 90e0273..72ef93d 100644
157--- a/testflinger_cli/client.py
158+++ b/testflinger_cli/client.py
159@@ -1,4 +1,4 @@
160-# Copyright (C) 2017-2019 Canonical
161+# Copyright (C) 2017-2020 Canonical
162 #
163 # This program is free software: you can redistribute it and/or modify
164 # it under the terms of the GNU General Public License as published by
165@@ -180,3 +180,12 @@ class Client():
166 return json.loads(data)
167 except ValueError:
168 return {}
169+
170+ def get_images(self, queue):
171+ """Get the advertised images from the testflinger server"""
172+ endpoint = '/v1/agents/images/' + queue
173+ data = self.get(endpoint)
174+ try:
175+ return json.loads(data)
176+ except ValueError:
177+ return {}

Subscribers

People subscribed via source and target branches