Merge lpbuildbot:use-wheels-to-pack into lpbuildbot:main

Proposed by Vaishnavi Asawale
Status: Work in progress
Proposed branch: lpbuildbot:use-wheels-to-pack
Merge into: lpbuildbot:main
Diff against target: 851 lines (+772/-0) (has conflicts)
12 files modified
.gitignore (+12/-0)
CONTRIBUTING.md (+34/-0)
LICENSE (+202/-0)
README.md (+36/-0)
charmcraft.yaml (+95/-0)
pyproject.toml (+46/-0)
requirements.txt (+48/-0)
src/charm.py (+137/-0)
templates/buildbot@.service.j2 (+18/-0)
tests/integration/test_charm.py (+34/-0)
tests/unit/test_charm.py (+24/-0)
tox.ini (+86/-0)
Conflict in .gitignore
Reviewer Review Type Date Requested Status
Canonical Launchpad Engineering Pending
Review via email: mp+492302@code.launchpad.net

Commit message

WIP: Use pre-downloaded wheels while packing the charm

To post a comment you must log in.

Unmerged commits

42a163e... by Vaishnavi Asawale

Can pack charm from wheels

1a74ee0... by Vaishnavi Asawale

Run ruff format and ruff check

4a31e14... by Vaishnavi Asawale

Final working charm

dfc5e83... by Vaishnavi Asawale

Working buildbot manager

0e69c8d... by Tushar Gupta

init

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index ae56c78..8b9db6a 100644
3--- a/.gitignore
4+++ b/.gitignore
5@@ -1,3 +1,4 @@
6+<<<<<<< .gitignore
7 tags
8 Session.vim
9 TAGS
10@@ -6,3 +7,14 @@ lp-db-devel-*
11 twistd.log*
12 twistd.pid
13 work
14+=======
15+venv/
16+build/
17+*.charm
18+.tox/
19+.coverage
20+__pycache__/
21+*.py[cod]
22+.idea
23+.vscode/
24+>>>>>>> .gitignore
25diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
26new file mode 100644
27index 0000000..20e88bc
28--- /dev/null
29+++ b/CONTRIBUTING.md
30@@ -0,0 +1,34 @@
31+# Contributing
32+
33+To make contributions to this charm, you'll need a working [development setup](https://juju.is/docs/sdk/dev-setup).
34+
35+You can create an environment for development with `tox`:
36+
37+```shell
38+tox devenv -e integration
39+source venv/bin/activate
40+```
41+
42+## Testing
43+
44+This project uses `tox` for managing test environments. There are some pre-configured environments
45+that can be used for linting and formatting code when you're preparing contributions to the charm:
46+
47+```shell
48+tox run -e format # update your code according to linting rules
49+tox run -e lint # code style
50+tox run -e static # static type checking
51+tox run -e unit # unit tests
52+tox run -e integration # integration tests
53+tox # runs 'format', 'lint', 'static', and 'unit' environments
54+```
55+
56+## Build the charm
57+
58+Build the charm in this git repository using:
59+
60+```shell
61+charmcraft pack
62+```
63+
64+<!-- You may want to include any contribution/style guidelines in this document>
65diff --git a/LICENSE b/LICENSE
66new file mode 100644
67index 0000000..bc0f920
68--- /dev/null
69+++ b/LICENSE
70@@ -0,0 +1,202 @@
71+
72+ Apache License
73+ Version 2.0, January 2004
74+ http://www.apache.org/licenses/
75+
76+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
77+
78+ 1. Definitions.
79+
80+ "License" shall mean the terms and conditions for use, reproduction,
81+ and distribution as defined by Sections 1 through 9 of this document.
82+
83+ "Licensor" shall mean the copyright owner or entity authorized by
84+ the copyright owner that is granting the License.
85+
86+ "Legal Entity" shall mean the union of the acting entity and all
87+ other entities that control, are controlled by, or are under common
88+ control with that entity. For the purposes of this definition,
89+ "control" means (i) the power, direct or indirect, to cause the
90+ direction or management of such entity, whether by contract or
91+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
92+ outstanding shares, or (iii) beneficial ownership of such entity.
93+
94+ "You" (or "Your") shall mean an individual or Legal Entity
95+ exercising permissions granted by this License.
96+
97+ "Source" form shall mean the preferred form for making modifications,
98+ including but not limited to software source code, documentation
99+ source, and configuration files.
100+
101+ "Object" form shall mean any form resulting from mechanical
102+ transformation or translation of a Source form, including but
103+ not limited to compiled object code, generated documentation,
104+ and conversions to other media types.
105+
106+ "Work" shall mean the work of authorship, whether in Source or
107+ Object form, made available under the License, as indicated by a
108+ copyright notice that is included in or attached to the work
109+ (an example is provided in the Appendix below).
110+
111+ "Derivative Works" shall mean any work, whether in Source or Object
112+ form, that is based on (or derived from) the Work and for which the
113+ editorial revisions, annotations, elaborations, or other modifications
114+ represent, as a whole, an original work of authorship. For the purposes
115+ of this License, Derivative Works shall not include works that remain
116+ separable from, or merely link (or bind by name) to the interfaces of,
117+ the Work and Derivative Works thereof.
118+
119+ "Contribution" shall mean any work of authorship, including
120+ the original version of the Work and any modifications or additions
121+ to that Work or Derivative Works thereof, that is intentionally
122+ submitted to Licensor for inclusion in the Work by the copyright owner
123+ or by an individual or Legal Entity authorized to submit on behalf of
124+ the copyright owner. For the purposes of this definition, "submitted"
125+ means any form of electronic, verbal, or written communication sent
126+ to the Licensor or its representatives, including but not limited to
127+ communication on electronic mailing lists, source code control systems,
128+ and issue tracking systems that are managed by, or on behalf of, the
129+ Licensor for the purpose of discussing and improving the Work, but
130+ excluding communication that is conspicuously marked or otherwise
131+ designated in writing by the copyright owner as "Not a Contribution."
132+
133+ "Contributor" shall mean Licensor and any individual or Legal Entity
134+ on behalf of whom a Contribution has been received by Licensor and
135+ subsequently incorporated within the Work.
136+
137+ 2. Grant of Copyright License. Subject to the terms and conditions of
138+ this License, each Contributor hereby grants to You a perpetual,
139+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
140+ copyright license to reproduce, prepare Derivative Works of,
141+ publicly display, publicly perform, sublicense, and distribute the
142+ Work and such Derivative Works in Source or Object form.
143+
144+ 3. Grant of Patent License. Subject to the terms and conditions of
145+ this License, each Contributor hereby grants to You a perpetual,
146+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
147+ (except as stated in this section) patent license to make, have made,
148+ use, offer to sell, sell, import, and otherwise transfer the Work,
149+ where such license applies only to those patent claims licensable
150+ by such Contributor that are necessarily infringed by their
151+ Contribution(s) alone or by combination of their Contribution(s)
152+ with the Work to which such Contribution(s) was submitted. If You
153+ institute patent litigation against any entity (including a
154+ cross-claim or counterclaim in a lawsuit) alleging that the Work
155+ or a Contribution incorporated within the Work constitutes direct
156+ or contributory patent infringement, then any patent licenses
157+ granted to You under this License for that Work shall terminate
158+ as of the date such litigation is filed.
159+
160+ 4. Redistribution. You may reproduce and distribute copies of the
161+ Work or Derivative Works thereof in any medium, with or without
162+ modifications, and in Source or Object form, provided that You
163+ meet the following conditions:
164+
165+ (a) You must give any other recipients of the Work or
166+ Derivative Works a copy of this License; and
167+
168+ (b) You must cause any modified files to carry prominent notices
169+ stating that You changed the files; and
170+
171+ (c) You must retain, in the Source form of any Derivative Works
172+ that You distribute, all copyright, patent, trademark, and
173+ attribution notices from the Source form of the Work,
174+ excluding those notices that do not pertain to any part of
175+ the Derivative Works; and
176+
177+ (d) If the Work includes a "NOTICE" text file as part of its
178+ distribution, then any Derivative Works that You distribute must
179+ include a readable copy of the attribution notices contained
180+ within such NOTICE file, excluding those notices that do not
181+ pertain to any part of the Derivative Works, in at least one
182+ of the following places: within a NOTICE text file distributed
183+ as part of the Derivative Works; within the Source form or
184+ documentation, if provided along with the Derivative Works; or,
185+ within a display generated by the Derivative Works, if and
186+ wherever such third-party notices normally appear. The contents
187+ of the NOTICE file are for informational purposes only and
188+ do not modify the License. You may add Your own attribution
189+ notices within Derivative Works that You distribute, alongside
190+ or as an addendum to the NOTICE text from the Work, provided
191+ that such additional attribution notices cannot be construed
192+ as modifying the License.
193+
194+ You may add Your own copyright statement to Your modifications and
195+ may provide additional or different license terms and conditions
196+ for use, reproduction, or distribution of Your modifications, or
197+ for any such Derivative Works as a whole, provided Your use,
198+ reproduction, and distribution of the Work otherwise complies with
199+ the conditions stated in this License.
200+
201+ 5. Submission of Contributions. Unless You explicitly state otherwise,
202+ any Contribution intentionally submitted for inclusion in the Work
203+ by You to the Licensor shall be under the terms and conditions of
204+ this License, without any additional terms or conditions.
205+ Notwithstanding the above, nothing herein shall supersede or modify
206+ the terms of any separate license agreement you may have executed
207+ with Licensor regarding such Contributions.
208+
209+ 6. Trademarks. This License does not grant permission to use the trade
210+ names, trademarks, service marks, or product names of the Licensor,
211+ except as required for reasonable and customary use in describing the
212+ origin of the Work and reproducing the content of the NOTICE file.
213+
214+ 7. Disclaimer of Warranty. Unless required by applicable law or
215+ agreed to in writing, Licensor provides the Work (and each
216+ Contributor provides its Contributions) on an "AS IS" BASIS,
217+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
218+ implied, including, without limitation, any warranties or conditions
219+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
220+ PARTICULAR PURPOSE. You are solely responsible for determining the
221+ appropriateness of using or redistributing the Work and assume any
222+ risks associated with Your exercise of permissions under this License.
223+
224+ 8. Limitation of Liability. In no event and under no legal theory,
225+ whether in tort (including negligence), contract, or otherwise,
226+ unless required by applicable law (such as deliberate and grossly
227+ negligent acts) or agreed to in writing, shall any Contributor be
228+ liable to You for damages, including any direct, indirect, special,
229+ incidental, or consequential damages of any character arising as a
230+ result of this License or out of the use or inability to use the
231+ Work (including but not limited to damages for loss of goodwill,
232+ work stoppage, computer failure or malfunction, or any and all
233+ other commercial damages or losses), even if such Contributor
234+ has been advised of the possibility of such damages.
235+
236+ 9. Accepting Warranty or Additional Liability. While redistributing
237+ the Work or Derivative Works thereof, You may choose to offer,
238+ and charge a fee for, acceptance of support, warranty, indemnity,
239+ or other liability obligations and/or rights consistent with this
240+ License. However, in accepting such obligations, You may act only
241+ on Your own behalf and on Your sole responsibility, not on behalf
242+ of any other Contributor, and only if You agree to indemnify,
243+ defend, and hold each Contributor harmless for any liability
244+ incurred by, or claims asserted against, such Contributor by reason
245+ of your accepting any such warranty or additional liability.
246+
247+ END OF TERMS AND CONDITIONS
248+
249+ APPENDIX: How to apply the Apache License to your work.
250+
251+ To apply the Apache License to your work, attach the following
252+ boilerplate notice, with the fields enclosed by brackets "[]"
253+ replaced with your own identifying information. (Don't include
254+ the brackets!) The text should be enclosed in the appropriate
255+ comment syntax for the file format. We also recommend that a
256+ file or class name and description of purpose be included on the
257+ same "printed page" as the copyright notice for easier
258+ identification within third-party archives.
259+
260+ Copyright 2025 Tushar Gupta
261+
262+ Licensed under the Apache License, Version 2.0 (the "License");
263+ you may not use this file except in compliance with the License.
264+ You may obtain a copy of the License at
265+
266+ http://www.apache.org/licenses/LICENSE-2.0
267+
268+ Unless required by applicable law or agreed to in writing, software
269+ distributed under the License is distributed on an "AS IS" BASIS,
270+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
271+ See the License for the specific language governing permissions and
272+ limitations under the License.
273diff --git a/README.md b/README.md
274new file mode 100644
275index 0000000..c53bbc8
276--- /dev/null
277+++ b/README.md
278@@ -0,0 +1,36 @@
279+<!--
280+Avoid using this README file for information that is maintained or published elsewhere, e.g.:
281+
282+* metadata.yaml > published on Charmhub
283+* documentation > published on (or linked to from) Charmhub
284+* detailed contribution guide > documentation or CONTRIBUTING.md
285+
286+Use links instead.
287+-->
288+
289+# buildbot-manager
290+
291+Charmhub package name: operator-template
292+More information: https://charmhub.io/buildbot-manager
293+
294+Describe your charm in one or two sentences.
295+
296+## Other resources
297+
298+<!-- If your charm is documented somewhere else other than Charmhub, provide a link separately. -->
299+
300+- [Read more](https://example.com)
301+
302+- [Contributing](CONTRIBUTING.md) <!-- or link to other contribution documentation -->
303+
304+- See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.
305+
306+# Setup Instructions
307+
308+To set up the development environment:
309+
310+```bash
311+python3 -m venv .venv
312+source .venv/bin/activate
313+pip install -r requirements.txt
314+```
315\ No newline at end of file
316diff --git a/charmcraft.yaml b/charmcraft.yaml
317new file mode 100644
318index 0000000..72c32d0
319--- /dev/null
320+++ b/charmcraft.yaml
321@@ -0,0 +1,95 @@
322+# This file configures Charmcraft.
323+# See https://juju.is/docs/sdk/charmcraft-config for guidance.
324+
325+
326+
327+# (Required)
328+name: buildbot-manager
329+
330+
331+# (Required)
332+type: charm
333+
334+
335+# (Recommended)
336+title: Buildbot Manager
337+
338+
339+# (Required)
340+summary: A very short one-line summary of the charm.
341+
342+
343+# (Required)
344+description: |
345+ A single sentence that says what the charm is, concisely and memorably.
346+
347+ A paragraph of one to three short sentences, that describe what the charm does.
348+
349+ A third paragraph that explains what need the charm meets.
350+
351+ Finally, a paragraph that describes whom the charm is useful for.
352+
353+
354+# (Required for 'charm' type)
355+# bases:
356+# - build-on:
357+# - name: ubuntu
358+# channel: "22.04"
359+# run-on:
360+# - name: ubuntu
361+# channel: "22.04"
362+
363+base: ubuntu@24.04
364+
365+platforms:
366+ amd64:
367+
368+parts:
369+ charm-wheels:
370+ plugin: dump
371+ source: https://git.launchpad.net/~vaishnavi-asawale/+git/wheels-buildbot-manager-charm
372+ source-commit: 1d1b984acbe3ee1fd523f25990887637e32fbeb6
373+ source-type: git
374+ organize:
375+ "*": wheels/
376+ prime:
377+ - "-wheels"
378+
379+ charm:
380+ source: .
381+ after:
382+ - charm-wheels
383+ build-packages:
384+ - build-essential
385+ - python3-dev
386+ - libssl-dev
387+ - libffi-dev
388+ - python3.12-venv
389+ stage:
390+ - wheels/*
391+ build-environment:
392+ - PIP_NO_INDEX: "true"
393+ - PIP_FIND_LINKS: "$CRAFT_STAGE/wheels"
394+ override-build: |
395+ INSTALL_PATH="/srv/buildbot/managers"
396+ VENV_NAME="sandbox"
397+ VENV_PATH="$INSTALL_PATH/$VENV_NAME"
398+ mkdir -p "$INSTALL_PATH"
399+ python3 -m venv --clear "$VENV_PATH"
400+ "$VENV_PATH/bin/pip" install --no-index --find-links="$CRAFT_STAGE/wheels" --requirement requirements.txt
401+
402+# (Optional) Configuration options for the charm
403+# This config section defines charm config options, and populates the Configure
404+# tab on Charmhub.
405+# More information on this section at https://juju.is/docs/sdk/charmcraft-yaml#heading--config
406+# General configuration documentation: https://juju.is/docs/sdk/config
407+config:
408+ options:
409+ # An example config option to customise the log level of the workload
410+ log-level:
411+ description: |
412+ Configures the log level of gunicorn.
413+
414+ Acceptable values are: "info", "debug", "warning", "error" and "critical"
415+ default: "info"
416+ type: string
417diff --git a/pyproject.toml b/pyproject.toml
418new file mode 100644
419index 0000000..e10531c
420--- /dev/null
421+++ b/pyproject.toml
422@@ -0,0 +1,46 @@
423+# Testing tools configuration
424+[tool.coverage.run]
425+branch = true
426+
427+[tool.coverage.report]
428+show_missing = true
429+
430+[tool.pytest.ini_options]
431+minversion = "6.0"
432+log_cli_level = "INFO"
433+
434+# Formatting tools configuration
435+[tool.black]
436+line-length = 99
437+target-version = ["py38"]
438+
439+# Linting tools configuration
440+[tool.ruff]
441+line-length = 99
442+select = ["E", "W", "F", "C", "N", "D", "I001"]
443+extend-ignore = [
444+ "D203",
445+ "D204",
446+ "D213",
447+ "D215",
448+ "D400",
449+ "D404",
450+ "D406",
451+ "D407",
452+ "D408",
453+ "D409",
454+ "D413",
455+]
456+ignore = ["E501", "D107"]
457+extend-exclude = ["__pycache__", "*.egg_info"]
458+per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]}
459+
460+[tool.ruff.mccabe]
461+max-complexity = 10
462+
463+[tool.codespell]
464+skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage"
465+
466+[tool.pyright]
467+include = ["src/**.py"]
468+
469diff --git a/requirements.txt b/requirements.txt
470new file mode 100644
471index 0000000..e81e315
472--- /dev/null
473+++ b/requirements.txt
474@@ -0,0 +1,48 @@
475+ops ~= 2.5
476+charmhelpers ~= 1.2
477+alembic==1.16.5
478+attrs==25.3.0
479+autobahn==24.4.2
480+Automat==25.4.16
481+buildbot==4.3.0
482+buildbot-console-view==4.3.0
483+buildbot-grid-view==4.3.0
484+buildbot-waterfall-view==4.3.0
485+buildbot-worker==4.3.0
486+buildbot-www==4.3.0
487+certifi==2025.8.3
488+cffi==1.17.1
489+charset-normalizer==3.4.3
490+constantly==23.10.4
491+croniter==6.0.0
492+cryptography==45.0.7
493+greenlet==3.2.4
494+hyperlink==21.0.0
495+idna==3.10
496+incremental==24.7.2
497+Jinja2==3.1.6
498+Mako==1.3.10
499+MarkupSafe==3.0.2
500+msgpack==1.1.1
501+multipart==1.3.0
502+packaging==25.0
503+pyasn1==0.6.1
504+pyasn1_modules==0.4.2
505+pycparser==2.22
506+PyJWT==2.10.1
507+pyOpenSSL==25.1.0
508+python-dateutil==2.9.0.post0
509+pytz==2025.2
510+PyYAML==6.0.2
511+requests==2.32.5
512+service-identity==24.2.0
513+setuptools==80.9.0
514+six==1.17.0
515+SQLAlchemy==2.0.43
516+treq==25.5.0
517+Twisted==25.5.0
518+txaio==25.6.1
519+typing_extensions==4.15.0
520+unidiff==0.7.5
521+urllib3==2.5.0
522+zope.interface==7.2
523diff --git a/src/charm.py b/src/charm.py
524new file mode 100755
525index 0000000..be5d93e
526--- /dev/null
527+++ b/src/charm.py
528@@ -0,0 +1,137 @@
529+#!/usr/bin/env python3
530+# Copyright 2025 Tushar Gupta
531+# See LICENSE file for licensing details.
532+
533+"""Charm the application."""
534+
535+import getpass
536+import logging
537+import os
538+import subprocess
539+
540+import ops
541+from charmhelpers.core import templating
542+
543+logger = logging.getLogger(__name__)
544+
545+class BuildbotManagerCharm(ops.CharmBase):
546+ """Charm the application."""
547+
548+ def __init__(self, framework: ops.Framework):
549+ super().__init__(framework)
550+ framework.observe(self.on.install, self._on_install)
551+ framework.observe(self.on.config_changed, self._on_config_changed)
552+ # working directory for the buildbot manager
553+ self._venv_name = "sandbox"
554+ self._install_path = "/srv/buildbot/managers"
555+ self._venv_path = os.path.join(self._install_path, self._venv_name)
556+ self._buildbot_bin_path = os.path.join(self._venv_path, "bin", "buildbot")
557+ self._pip_bin_path = os.path.join(self._venv_path, "bin", "pip")
558+
559+ def _run_command(self, command: list[str], message, cwd=None, within_venv=False):
560+ # See comment from Dima (element)
561+ # TODO: REmove all venv land ogic, unset PYTHON_PATH and LD_LIBRAR before calling
562+ # commands from inside the virtualenv and reset them.Y_PATH"""Utility to run shell commands with error handling."""
563+ shell_command = ""
564+ try:
565+ self.unit.status = ops.MaintenanceStatus(message)
566+ if within_venv:
567+ command = [
568+ "source",
569+ os.path.join(self._venv_path, "bin", "activate"),
570+ "&&",
571+ ] + command
572+ shell_command = " ".join(command)
573+
574+ # Log current user, path, and PYTHONPATH
575+ logger.debug("Current user: %s", getpass.getuser())
576+ logger.debug("Current working directory: %s", os.getcwd())
577+ logger.debug("PYTHONPATH: %s", os.environ.get("PYTHONPATH", ""))
578+
579+ logger.debug("Trying to run '%s'", shell_command)
580+
581+ # nit: shell=True can lead to security implications,
582+ res = subprocess.run(
583+ shell_command,
584+ cwd=cwd,
585+ executable="/bin/bash",
586+ stdout=subprocess.PIPE,
587+ stderr=subprocess.PIPE,
588+ text=True if within_venv else False,
589+ shell=True,
590+ check=True,
591+ )
592+
593+ logger.debug("Command stdout:\n%s", res.stdout.strip())
594+ logger.debug("Command stderr:\n%s", res.stderr.strip())
595+ logger.debug(
596+ "Successfully ran '%s' with return code %s", shell_command, res.returncode
597+ )
598+
599+ except subprocess.CalledProcessError as e:
600+ logger.error("Failed to run '%s'", shell_command)
601+ logger.error("Return code: %s", e.returncode)
602+ logger.error("stdout:\n%s", e.stdout)
603+ logger.error("stderr:\n%s", e.stderr)
604+ self.unit.status = ops.BlockedStatus(f"Failure running: {message}")
605+ raise RuntimeError(f"Command failed: {shell_command}") from e
606+
607+ def _on_install(self, event: ops.InstallEvent):
608+ """Handle install event: set up Buildbot manager environment."""
609+ # nit: use pathlib
610+ self._run_command(["mkdir", "-p", self._install_path], "Creating manager root directory")
611+
612+ self._run_command(
613+ ["python3", "-m", "venv", self._venv_path],
614+ "Creating virtualenv",
615+ cwd=self._install_path,
616+ )
617+
618+ # check if using cwd helps
619+ self._run_command(
620+ ["pip", "install", "--upgrade", "pip"],
621+ "Upgrading pip",
622+ cwd=self._install_path,
623+ within_venv=True,
624+ )
625+
626+ templating.render("buildbot@.service.j2", "/lib/systemd/system/buildbot@.service", {})
627+
628+ self._run_command(["systemctl", "daemon-reload"], message="Reload systemd")
629+
630+ self._run_command(
631+ ["adduser", "--system", "--home", "/srv/buildbot", "--group", "buildbot"],
632+ "Creating buildbot user",
633+ )
634+
635+ def _on_config_changed(self, event: ops.ConfigChangedEvent):
636+ service_name = "lp-manager"
637+ self._run_command(
638+ ["buildbot", "create-master", service_name],
639+ "Creating Buildbot manager",
640+ cwd=self._install_path,
641+ within_venv=True,
642+ )
643+ cfg_path = os.path.join(self._install_path, service_name)
644+ # think about what configurations this charm will need
645+ self._run_command(
646+ ["chown", "-R", "buildbot:buildbot", cfg_path],
647+ "Set ownership of Buildbot manager directory",
648+ )
649+
650+ self._run_command(
651+ ["mv", "master.cfg.sample", "master.cfg"],
652+ "Configuring master",
653+ cwd=cfg_path,
654+ )
655+
656+ self._run_command(
657+ ["systemctl", "enable", "--now", f"buildbot@{service_name}.service"],
658+ message="Enabling and starting buildbot manager",
659+ )
660+
661+ self.unit.status = ops.ActiveStatus("Buildbot master is running")
662+
663+
664+if __name__ == "__main__": # pragma: nocover
665+ ops.main(BuildbotManagerCharm) # type: ignore
666diff --git a/templates/buildbot@.service.j2 b/templates/buildbot@.service.j2
667new file mode 100644
668index 0000000..a5e9103
669--- /dev/null
670+++ b/templates/buildbot@.service.j2
671@@ -0,0 +1,18 @@
672+[Unit]
673+Description=Buildbot master %I
674+Documentation=https://docs.buildbot.net/
675+ConditionDirectoryNotEmpty=/srv/buildbot/managers/%i
676+ConditionFileNotEmpty=/srv/buildbot/managers/%i/buildbot.tac
677+ConditionFileNotEmpty=/srv/buildbot/managers/%i/master.cfg
678+
679+[Service]
680+Type=simple
681+WorkingDirectory=/srv/buildbot/managers/%i
682+PrivateTmp=true
683+User=buildbot
684+Group=buildbot
685+ExecStart=/srv/buildbot/managers/sandbox/bin/buildbot start --nodaemon /srv/buildbot/managers/%i
686+ExecReload=/bin/kill -HUP $MAINPID
687+
688+[Install]
689+WantedBy=multi-user.target
690diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py
691new file mode 100644
692index 0000000..c9da586
693--- /dev/null
694+++ b/tests/integration/test_charm.py
695@@ -0,0 +1,34 @@
696+#!/usr/bin/env python3
697+# Copyright 2025 Tushar Gupta
698+# See LICENSE file for licensing details.
699+
700+import asyncio
701+import logging
702+from pathlib import Path
703+
704+import pytest
705+import yaml
706+from pytest_operator.plugin import OpsTest
707+
708+logger = logging.getLogger(__name__)
709+
710+METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
711+APP_NAME = METADATA["name"]
712+
713+
714+@pytest.mark.abort_on_fail
715+async def test_build_and_deploy(ops_test: OpsTest):
716+ """Build the charm-under-test and deploy it together with related charms.
717+
718+ Assert on the unit status before any relations/configurations take place.
719+ """
720+ # Build and deploy charm from local source folder
721+ charm = await ops_test.build_charm(".")
722+
723+ # Deploy the charm and wait for active/idle status
724+ await asyncio.gather(
725+ ops_test.model.deploy(charm, application_name=APP_NAME),
726+ ops_test.model.wait_for_idle(
727+ apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000
728+ ),
729+ )
730diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
731new file mode 100644
732index 0000000..f16045e
733--- /dev/null
734+++ b/tests/unit/test_charm.py
735@@ -0,0 +1,24 @@
736+# Copyright 2025 Tushar Gupta
737+# See LICENSE file for licensing details.
738+#
739+# Learn more about testing at: https://juju.is/docs/sdk/testing
740+
741+import unittest
742+
743+import ops
744+import ops.testing
745+
746+from charm import BuildbotManagerCharm
747+
748+
749+class TestCharm(unittest.TestCase):
750+ def setUp(self):
751+ self.harness = ops.testing.Harness(BuildbotManagerCharm)
752+ self.addCleanup(self.harness.cleanup)
753+
754+ def test_start(self):
755+ # Simulate the charm starting
756+ self.harness.begin_with_initial_hooks()
757+
758+ # Ensure we set an ActiveStatus with no message
759+ self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus())
760diff --git a/tox.ini b/tox.ini
761new file mode 100644
762index 0000000..db6bb52
763--- /dev/null
764+++ b/tox.ini
765@@ -0,0 +1,86 @@
766+# Copyright 2025 Tushar Gupta
767+# See LICENSE file for licensing details.
768+
769+[tox]
770+no_package = True
771+skip_missing_interpreters = True
772+env_list = format, lint, static, unit
773+min_version = 4.0.0
774+
775+[vars]
776+src_path = {tox_root}/src
777+tests_path = {tox_root}/tests
778+;lib_path = {tox_root}/lib/charms/operator_name_with_underscores
779+all_path = {[vars]src_path} {[vars]tests_path}
780+
781+[testenv]
782+set_env =
783+ PYTHONPATH = {tox_root}/lib:{[vars]src_path}
784+ PYTHONBREAKPOINT=pdb.set_trace
785+ PY_COLORS=1
786+pass_env =
787+ PYTHONPATH
788+ CHARM_BUILD_DIR
789+ MODEL_SETTINGS
790+
791+[testenv:format]
792+description = Apply coding style standards to code
793+deps =
794+ black
795+ ruff
796+commands =
797+ black {[vars]all_path}
798+ ruff check --fix {[vars]all_path}
799+
800+[testenv:lint]
801+description = Check code against coding style standards
802+deps =
803+ black
804+ ruff
805+ codespell
806+commands =
807+ # if this charm owns a lib, uncomment "lib_path" variable
808+ # and uncomment the following line
809+ # codespell {[vars]lib_path}
810+ codespell {tox_root}
811+ ruff check {[vars]all_path}
812+ black --check --diff {[vars]all_path}
813+
814+[testenv:unit]
815+description = Run unit tests
816+deps =
817+ pytest
818+ coverage[toml]
819+ -r {tox_root}/requirements.txt
820+commands =
821+ coverage run --source={[vars]src_path} \
822+ -m pytest \
823+ --tb native \
824+ -v \
825+ -s \
826+ {posargs} \
827+ {[vars]tests_path}/unit
828+ coverage report
829+
830+[testenv:static]
831+description = Run static type checks
832+deps =
833+ pyright
834+ -r {tox_root}/requirements.txt
835+commands =
836+ pyright {posargs}
837+
838+[testenv:integration]
839+description = Run integration tests
840+deps =
841+ pytest
842+ juju
843+ pytest-operator
844+ -r {tox_root}/requirements.txt
845+commands =
846+ pytest -v \
847+ -s \
848+ --tb native \
849+ --log-cli-level=INFO \
850+ {posargs} \
851+ {[vars]tests_path}/integration

Subscribers

People subscribed via source and target branches