diff --git a/doc/source/operation.rst b/doc/source/operation.rst index bb4bfecb2..33d315e11 100644 --- a/doc/source/operation.rst +++ b/doc/source/operation.rst @@ -149,9 +149,9 @@ dib-image-list .. program-output:: nodepool dib-image-list --help :nostderr: -dib-request-list -^^^^^^^^^^^^^^^^ -.. program-output:: nodepool dib-request-list --help +image-status +^^^^^^^^^^^^ +.. program-output:: nodepool image-status --help :nostderr: image-list @@ -396,9 +396,9 @@ launchers, all will provide the same information. :resheader Content-Type: ``application/json`` or ``text/plain`` depending on the :http:header:`Accept` header -.. http:get:: /dib-request-list +.. http:get:: /image-status - The status of manual build requests + The paused and manual build status of images :query fields: comma-separated list of fields to display :reqheader Accept: ``application/json`` or ``text/*`` diff --git a/nodepool/cmd/nodepoolcmd.py b/nodepool/cmd/nodepoolcmd.py index b585d8fa6..97b32123e 100644 --- a/nodepool/cmd/nodepoolcmd.py +++ b/nodepool/cmd/nodepoolcmd.py @@ -61,10 +61,10 @@ class NodePoolCmd(NodepoolApp): help='list images built with diskimage-builder') cmd_dib_image_list.set_defaults(func=self.dib_image_list) - cmd_dib_request_list = subparsers.add_parser( - 'dib-request-list', - help='list image build requests') - cmd_dib_request_list.set_defaults(func=self.dib_request_list) + cmd_image_status = subparsers.add_parser( + 'image-status', + help='list image status') + cmd_image_status.set_defaults(func=self.image_status) cmd_image_build = subparsers.add_parser( 'image-build', @@ -208,8 +208,8 @@ class NodePoolCmd(NodepoolApp): results = status.dib_image_list(self.zk) print(status.output(results, 'pretty')) - def dib_request_list(self): - results = status.dib_request_list(self.zk) + def image_status(self): + results = status.image_status(self.zk) print(status.output(results, 'pretty')) def image_list(self): @@ -431,7 +431,7 @@ class NodePoolCmd(NodepoolApp): # commands needing ZooKeeper if self.args.command in ('image-build', 'dib-image-list', - 'dib-request-list', + 'image-status', 'image-list', 'dib-image-delete', 'image-delete', 'alien-image-list', 'list', 'delete', diff --git a/nodepool/status.py b/nodepool/status.py index ba4712396..1df4e46e0 100644 --- a/nodepool/status.py +++ b/nodepool/status.py @@ -71,7 +71,7 @@ def _to_pretty_table(objs, headers_table, fields): for k in headers_table: if fields and k not in fields: continue - if k == 'age': + if k == 'age' or k.endswith('_age') and obj[k] is not None: try: obj_age = age(int(obj[k])) except ValueError: @@ -81,6 +81,8 @@ def _to_pretty_table(objs, headers_table, fields): else: if isinstance(obj[k], list): values.append(','.join(obj[k])) + elif obj[k] is None: + values.append('') else: values.append(obj[k]) t.add_row(values) @@ -202,21 +204,28 @@ def dib_image_list(zk): return (objs, headers_table) -def dib_request_list(zk): +def image_status(zk): headers_table = OrderedDict([ ("image", "Image"), - ("state", "State"), - ("age", "Age") + ("paused", "Paused"), + ("build_request", "Build Request"), + ("build_request_age", "Build Request Age") ]) objs = [] for image_name in zk.getImageNames(): request = zk.getBuildRequest(image_name) - if request is None: - continue + paused = zk.getImagePaused(image_name) + if request: + age = int(request.state_time) + req = 'pending' if request.pending else 'building' + else: + age = None + req = None objs.append({ - "image": request.image_name, - "state": "pending" if request.pending else "building", - "age": int(request.state_time) + "image": image_name, + "paused": bool(paused), + "build_request": req, + "build_request_age": age, }) return (objs, headers_table) diff --git a/nodepool/tests/unit/test_commands.py b/nodepool/tests/unit/test_commands.py index 160fe0616..e7c1f44c1 100644 --- a/nodepool/tests/unit/test_commands.py +++ b/nodepool/tests/unit/test_commands.py @@ -193,7 +193,7 @@ class TestNodepoolCMD(tests.DBTestCase): nodepoolcmd.main() self.assert_listed(configfile, ['dib-image-list'], 4, zk.READY, 1) - def test_dib_request_list(self): + def test_image_status(self): configfile = self.setup_config('node.yaml') builder = self.useBuilder(configfile) # Make sure we have enough time to test for the build request @@ -202,7 +202,7 @@ class TestNodepoolCMD(tests.DBTestCase): worker._interval = 60 self.waitForImage('fake-provider', 'fake-image') self.zk.submitBuildRequest("fake-image") - self.assert_listed(configfile, ['dib-request-list'], + self.assert_listed(configfile, ['image-status'], 0, 'fake-image', 1) def test_dib_image_build_pause(self): diff --git a/nodepool/tests/unit/test_webapp.py b/nodepool/tests/unit/test_webapp.py index dc41aa539..34e9b0891 100644 --- a/nodepool/tests/unit/test_webapp.py +++ b/nodepool/tests/unit/test_webapp.py @@ -138,7 +138,7 @@ class TestWebApp(tests.DBTestCase): 'formats': ['qcow2'], 'state': 'ready'}, objs[0]) - def test_dib_request_list_json(self): + def test_image_status_json(self): configfile = self.setup_config("node.yaml") pool = self.useNodepool(configfile, watermark_sleep=1) builder = self.useBuilder(configfile) @@ -155,10 +155,8 @@ class TestWebApp(tests.DBTestCase): self.waitForImage("fake-provider", "fake-image") self.waitForNodes('fake-label') - self.zk.submitBuildRequest("fake-image") - req = request.Request( - "http://localhost:{}/dib-request-list".format(port)) + "http://localhost:{}/image-status".format(port)) req.add_header("Accept", "application/json") f = request.urlopen(req) @@ -168,7 +166,18 @@ class TestWebApp(tests.DBTestCase): data = f.read() objs = json.loads(data.decode("utf8")) self.assertDictContainsSubset({"image": "fake-image", - "state": "pending"}, objs[0]) + "paused": False, + "build_request": None}, objs[0]) + + self.zk.submitBuildRequest("fake-image") + + webapp.cache.cache.clear() + f = request.urlopen(req) + data = f.read() + objs = json.loads(data.decode("utf8")) + self.assertDictContainsSubset({"image": "fake-image", + "paused": False, + "build_request": "pending"}, objs[0]) webapp.cache.cache.clear() with self.zk.imageBuildLock('fake-image', blocking=True, timeout=1): @@ -177,7 +186,8 @@ class TestWebApp(tests.DBTestCase): objs = json.loads(data.decode("utf8")) self.assertDictContainsSubset({"image": "fake-image", - "state": "building"}, objs[0]) + "paused": False, + "build_request": "building"}, objs[0]) def test_node_list_json(self): configfile = self.setup_config('node.yaml') diff --git a/nodepool/webapp.py b/nodepool/webapp.py index 0a505632e..d1de4ea43 100644 --- a/nodepool/webapp.py +++ b/nodepool/webapp.py @@ -101,8 +101,8 @@ class WebApp(threading.Thread): results = status.image_list(zk) elif path == '/dib-image-list': results = status.dib_image_list(zk) - elif path == '/dib-request-list': - results = status.dib_request_list(zk) + elif path == '/image-status': + results = status.image_status(zk) elif path == '/node-list': results = status.node_list(zk, node_id=params.get('node_id')) diff --git a/releasenotes/notes/image-status-bedca1dd0184c14b.yaml b/releasenotes/notes/image-status-bedca1dd0184c14b.yaml new file mode 100644 index 000000000..4562888be --- /dev/null +++ b/releasenotes/notes/image-status-bedca1dd0184c14b.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + A new `image-status` command and accompanying web endpoint are + available to easily see what images have been paused via the + `image-pause` command and have pending manual build requests via + the `build-image` command.