Merge ~barryprice/charm-tor/+git/charm-tor:main into charm-tor:main

Proposed by Barry Price
Status: Work in progress
Proposed branch: ~barryprice/charm-tor/+git/charm-tor:main
Merge into: charm-tor:main
Diff against target: 2994 lines (+2405/-408)
15 files modified
.gitignore (+6/-3)
LICENSE (+674/-202)
Makefile (+21/-0)
README.md (+8/-16)
charmcraft.yaml (+3/-3)
config.yaml (+31/-12)
dev/null (+0/-16)
lib/charms/operator_libs_linux/v0/apt.py (+1329/-0)
metadata.yaml (+12/-21)
pyproject.toml (+38/-0)
src/charm.py (+184/-82)
templates/torrc (+12/-0)
tests/__init__.py (+4/-0)
tests/test_charm.py (+5/-53)
tox.ini (+78/-0)
Reviewer Review Type Date Requested Status
Tor Charmers Pending
Review via email: mp+425423@code.launchpad.net

Commit message

WIP MP to replace the charmcraft boilerplate with a functional charm

To post a comment you must log in.
d39909c... by Barry Price

Add lib, pyproject, tox, refresh ,gitignore

47bd6c5... by Barry Price

Initial Makefile

6264307... by Barry Price

Add paths, lint target, run lint

1d7bc86... by Barry Price

Handle upstream torproject repo installation/removal

d762c48... by Barry Price

Ensure packages are installed on hooks, sort package defs

2892df0... by Barry Price

Bugfixes

6b266a9... by Barry Price

Still WIP but a clean deploy with multiple related services works after a fashion (but relation changes fail catastrophically)

05532b8... by Barry Price

Restore boilerplate

Unmerged commits

05532b8... by Barry Price

Restore boilerplate

6b266a9... by Barry Price

Still WIP but a clean deploy with multiple related services works after a fashion (but relation changes fail catastrophically)

2892df0... by Barry Price

Bugfixes

d762c48... by Barry Price

Ensure packages are installed on hooks, sort package defs

1d7bc86... by Barry Price

Handle upstream torproject repo installation/removal

6264307... by Barry Price

Add paths, lint target, run lint

47bd6c5... by Barry Price

Initial Makefile

d39909c... by Barry Price

Add lib, pyproject, tox, refresh ,gitignore

4220710... by Barry Price

Basic skeleton, remove most boilerplate, no functionality yet

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.gitignore b/.gitignore
index 2c3f0e5..69a2281 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,10 @@
1venv/1/build
2build/2/venv
3
3*.charm4*.charm
5*.py[cod]
6*.swp
47
5.coverage8.coverage
9.tox
6__pycache__/10__pycache__/
7*.py[cod]
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
8deleted file mode 10064411deleted file mode 100644
index f5dac3b..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,34 +0,0 @@
1# charm-tor
2
3## Developing
4
5Create and activate a virtualenv with the development requirements:
6
7 virtualenv -p python3 venv
8 source venv/bin/activate
9 pip install -r requirements-dev.txt
10
11## Code overview
12
13TEMPLATE-TODO:
14One of the most important things a consumer of your charm (or library)
15needs to know is what set of functionality it provides. Which categories
16does it fit into? Which events do you listen to? Which libraries do you
17consume? Which ones do you export and how are they used?
18
19## Intended use case
20
21TEMPLATE-TODO:
22Why were these decisions made? What's the scope of your charm?
23
24## Roadmap
25
26If this Charm doesn't fulfill all of the initial functionality you were
27hoping for or planning on, please add a Roadmap or TODO here
28
29## Testing
30
31The Python operator framework includes a very nice harness for testing
32operator behaviour without full deployment. Just `run_tests`:
33
34 ./run_tests
diff --git a/LICENSE b/LICENSE
index d645695..f288702 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,202 +1,674 @@
11 GNU GENERAL PUBLIC LICENSE
2 Apache License2 Version 3, 29 June 2007
3 Version 2.0, January 20043
4 http://www.apache.org/licenses/4 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
55 Everyone is permitted to copy and distribute verbatim copies
6 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION6 of this license document, but changing it is not allowed.
77
8 1. Definitions.8 Preamble
99
10 "License" shall mean the terms and conditions for use, reproduction,10 The GNU General Public License is a free, copyleft license for
11 and distribution as defined by Sections 1 through 9 of this document.11software and other kinds of works.
1212
13 "Licensor" shall mean the copyright owner or entity authorized by13 The licenses for most software and other practical works are designed
14 the copyright owner that is granting the License.14to take away your freedom to share and change the works. By contrast,
1515the GNU General Public License is intended to guarantee your freedom to
16 "Legal Entity" shall mean the union of the acting entity and all16share and change all versions of a program--to make sure it remains free
17 other entities that control, are controlled by, or are under common17software for all its users. We, the Free Software Foundation, use the
18 control with that entity. For the purposes of this definition,18GNU General Public License for most of our software; it applies also to
19 "control" means (i) the power, direct or indirect, to cause the19any other work released this way by its authors. You can apply it to
20 direction or management of such entity, whether by contract or20your programs, too.
21 otherwise, or (ii) ownership of fifty percent (50%) or more of the21
22 outstanding shares, or (iii) beneficial ownership of such entity.22 When we speak of free software, we are referring to freedom, not
2323price. Our General Public Licenses are designed to make sure that you
24 "You" (or "Your") shall mean an individual or Legal Entity24have the freedom to distribute copies of free software (and charge for
25 exercising permissions granted by this License.25them if you wish), that you receive source code or can get it if you
2626want it, that you can change the software or use pieces of it in new
27 "Source" form shall mean the preferred form for making modifications,27free programs, and that you know you can do these things.
28 including but not limited to software source code, documentation28
29 source, and configuration files.29 To protect your rights, we need to prevent others from denying you
3030these rights or asking you to surrender the rights. Therefore, you have
31 "Object" form shall mean any form resulting from mechanical31certain responsibilities if you distribute copies of the software, or if
32 transformation or translation of a Source form, including but32you modify it: responsibilities to respect the freedom of others.
33 not limited to compiled object code, generated documentation,33
34 and conversions to other media types.34 For example, if you distribute copies of such a program, whether
3535gratis or for a fee, you must pass on to the recipients the same
36 "Work" shall mean the work of authorship, whether in Source or36freedoms that you received. You must make sure that they, too, receive
37 Object form, made available under the License, as indicated by a37or can get the source code. And you must show them these terms so they
38 copyright notice that is included in or attached to the work38know their rights.
39 (an example is provided in the Appendix below).39
4040 Developers that use the GNU GPL protect your rights with two steps:
41 "Derivative Works" shall mean any work, whether in Source or Object41(1) assert copyright on the software, and (2) offer you this License
42 form, that is based on (or derived from) the Work and for which the42giving you legal permission to copy, distribute and/or modify it.
43 editorial revisions, annotations, elaborations, or other modifications43
44 represent, as a whole, an original work of authorship. For the purposes44 For the developers' and authors' protection, the GPL clearly explains
45 of this License, Derivative Works shall not include works that remain45that there is no warranty for this free software. For both users' and
46 separable from, or merely link (or bind by name) to the interfaces of,46authors' sake, the GPL requires that modified versions be marked as
47 the Work and Derivative Works thereof.47changed, so that their problems will not be attributed erroneously to
4848authors of previous versions.
49 "Contribution" shall mean any work of authorship, including49
50 the original version of the Work and any modifications or additions50 Some devices are designed to deny users access to install or run
51 to that Work or Derivative Works thereof, that is intentionally51modified versions of the software inside them, although the manufacturer
52 submitted to Licensor for inclusion in the Work by the copyright owner52can do so. This is fundamentally incompatible with the aim of
53 or by an individual or Legal Entity authorized to submit on behalf of53protecting users' freedom to change the software. The systematic
54 the copyright owner. For the purposes of this definition, "submitted"54pattern of such abuse occurs in the area of products for individuals to
55 means any form of electronic, verbal, or written communication sent55use, which is precisely where it is most unacceptable. Therefore, we
56 to the Licensor or its representatives, including but not limited to56have designed this version of the GPL to prohibit the practice for those
57 communication on electronic mailing lists, source code control systems,57products. If such problems arise substantially in other domains, we
58 and issue tracking systems that are managed by, or on behalf of, the58stand ready to extend this provision to those domains in future versions
59 Licensor for the purpose of discussing and improving the Work, but59of the GPL, as needed to protect the freedom of users.
60 excluding communication that is conspicuously marked or otherwise60
61 designated in writing by the copyright owner as "Not a Contribution."61 Finally, every program is threatened constantly by software patents.
6262States should not allow patents to restrict development and use of
63 "Contributor" shall mean Licensor and any individual or Legal Entity63software on general-purpose computers, but in those that do, we wish to
64 on behalf of whom a Contribution has been received by Licensor and64avoid the special danger that patents applied to a free program could
65 subsequently incorporated within the Work.65make it effectively proprietary. To prevent this, the GPL assures that
6666patents cannot be used to render the program non-free.
67 2. Grant of Copyright License. Subject to the terms and conditions of67
68 this License, each Contributor hereby grants to You a perpetual,68 The precise terms and conditions for copying, distribution and
69 worldwide, non-exclusive, no-charge, royalty-free, irrevocable69modification follow.
70 copyright license to reproduce, prepare Derivative Works of,70
71 publicly display, publicly perform, sublicense, and distribute the71 TERMS AND CONDITIONS
72 Work and such Derivative Works in Source or Object form.72
7373 0. Definitions.
74 3. Grant of Patent License. Subject to the terms and conditions of74
75 this License, each Contributor hereby grants to You a perpetual,75 "This License" refers to version 3 of the GNU General Public License.
76 worldwide, non-exclusive, no-charge, royalty-free, irrevocable76
77 (except as stated in this section) patent license to make, have made,77 "Copyright" also means copyright-like laws that apply to other kinds of
78 use, offer to sell, sell, import, and otherwise transfer the Work,78works, such as semiconductor masks.
79 where such license applies only to those patent claims licensable79
80 by such Contributor that are necessarily infringed by their80 "The Program" refers to any copyrightable work licensed under this
81 Contribution(s) alone or by combination of their Contribution(s)81License. Each licensee is addressed as "you". "Licensees" and
82 with the Work to which such Contribution(s) was submitted. If You82"recipients" may be individuals or organizations.
83 institute patent litigation against any entity (including a83
84 cross-claim or counterclaim in a lawsuit) alleging that the Work84 To "modify" a work means to copy from or adapt all or part of the work
85 or a Contribution incorporated within the Work constitutes direct85in a fashion requiring copyright permission, other than the making of an
86 or contributory patent infringement, then any patent licenses86exact copy. The resulting work is called a "modified version" of the
87 granted to You under this License for that Work shall terminate87earlier work or a work "based on" the earlier work.
88 as of the date such litigation is filed.88
8989 A "covered work" means either the unmodified Program or a work based
90 4. Redistribution. You may reproduce and distribute copies of the90on the Program.
91 Work or Derivative Works thereof in any medium, with or without91
92 modifications, and in Source or Object form, provided that You92 To "propagate" a work means to do anything with it that, without
93 meet the following conditions:93permission, would make you directly or secondarily liable for
9494infringement under applicable copyright law, except executing it on a
95 (a) You must give any other recipients of the Work or95computer or modifying a private copy. Propagation includes copying,
96 Derivative Works a copy of this License; and96distribution (with or without modification), making available to the
9797public, and in some countries other activities as well.
98 (b) You must cause any modified files to carry prominent notices98
99 stating that You changed the files; and99 To "convey" a work means any kind of propagation that enables other
100100parties to make or receive copies. Mere interaction with a user through
101 (c) You must retain, in the Source form of any Derivative Works101a computer network, with no transfer of a copy, is not conveying.
102 that You distribute, all copyright, patent, trademark, and102
103 attribution notices from the Source form of the Work,103 An interactive user interface displays "Appropriate Legal Notices"
104 excluding those notices that do not pertain to any part of104to the extent that it includes a convenient and prominently visible
105 the Derivative Works; and105feature that (1) displays an appropriate copyright notice, and (2)
106106tells the user that there is no warranty for the work (except to the
107 (d) If the Work includes a "NOTICE" text file as part of its107extent that warranties are provided), that licensees may convey the
108 distribution, then any Derivative Works that You distribute must108work under this License, and how to view a copy of this License. If
109 include a readable copy of the attribution notices contained109the interface presents a list of user commands or options, such as a
110 within such NOTICE file, excluding those notices that do not110menu, a prominent item in the list meets this criterion.
111 pertain to any part of the Derivative Works, in at least one111
112 of the following places: within a NOTICE text file distributed112 1. Source Code.
113 as part of the Derivative Works; within the Source form or113
114 documentation, if provided along with the Derivative Works; or,114 The "source code" for a work means the preferred form of the work
115 within a display generated by the Derivative Works, if and115for making modifications to it. "Object code" means any non-source
116 wherever such third-party notices normally appear. The contents116form of a work.
117 of the NOTICE file are for informational purposes only and117
118 do not modify the License. You may add Your own attribution118 A "Standard Interface" means an interface that either is an official
119 notices within Derivative Works that You distribute, alongside119standard defined by a recognized standards body, or, in the case of
120 or as an addendum to the NOTICE text from the Work, provided120interfaces specified for a particular programming language, one that
121 that such additional attribution notices cannot be construed121is widely used among developers working in that language.
122 as modifying the License.122
123123 The "System Libraries" of an executable work include anything, other
124 You may add Your own copyright statement to Your modifications and124than the work as a whole, that (a) is included in the normal form of
125 may provide additional or different license terms and conditions125packaging a Major Component, but which is not part of that Major
126 for use, reproduction, or distribution of Your modifications, or126Component, and (b) serves only to enable use of the work with that
127 for any such Derivative Works as a whole, provided Your use,127Major Component, or to implement a Standard Interface for which an
128 reproduction, and distribution of the Work otherwise complies with128implementation is available to the public in source code form. A
129 the conditions stated in this License.129"Major Component", in this context, means a major essential component
130130(kernel, window system, and so on) of the specific operating system
131 5. Submission of Contributions. Unless You explicitly state otherwise,131(if any) on which the executable work runs, or a compiler used to
132 any Contribution intentionally submitted for inclusion in the Work132produce the work, or an object code interpreter used to run it.
133 by You to the Licensor shall be under the terms and conditions of133
134 this License, without any additional terms or conditions.134 The "Corresponding Source" for a work in object code form means all
135 Notwithstanding the above, nothing herein shall supersede or modify135the source code needed to generate, install, and (for an executable
136 the terms of any separate license agreement you may have executed136work) run the object code and to modify the work, including scripts to
137 with Licensor regarding such Contributions.137control those activities. However, it does not include the work's
138138System Libraries, or general-purpose tools or generally available free
139 6. Trademarks. This License does not grant permission to use the trade139programs which are used unmodified in performing those activities but
140 names, trademarks, service marks, or product names of the Licensor,140which are not part of the work. For example, Corresponding Source
141 except as required for reasonable and customary use in describing the141includes interface definition files associated with source files for
142 origin of the Work and reproducing the content of the NOTICE file.142the work, and the source code for shared libraries and dynamically
143143linked subprograms that the work is specifically designed to require,
144 7. Disclaimer of Warranty. Unless required by applicable law or144such as by intimate data communication or control flow between those
145 agreed to in writing, Licensor provides the Work (and each145subprograms and other parts of the work.
146 Contributor provides its Contributions) on an "AS IS" BASIS,146
147 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or147 The Corresponding Source need not include anything that users
148 implied, including, without limitation, any warranties or conditions148can regenerate automatically from other parts of the Corresponding
149 of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A149Source.
150 PARTICULAR PURPOSE. You are solely responsible for determining the150
151 appropriateness of using or redistributing the Work and assume any151 The Corresponding Source for a work in source code form is that
152 risks associated with Your exercise of permissions under this License.152same work.
153153
154 8. Limitation of Liability. In no event and under no legal theory,154 2. Basic Permissions.
155 whether in tort (including negligence), contract, or otherwise,155
156 unless required by applicable law (such as deliberate and grossly156 All rights granted under this License are granted for the term of
157 negligent acts) or agreed to in writing, shall any Contributor be157copyright on the Program, and are irrevocable provided the stated
158 liable to You for damages, including any direct, indirect, special,158conditions are met. This License explicitly affirms your unlimited
159 incidental, or consequential damages of any character arising as a159permission to run the unmodified Program. The output from running a
160 result of this License or out of the use or inability to use the160covered work is covered by this License only if the output, given its
161 Work (including but not limited to damages for loss of goodwill,161content, constitutes a covered work. This License acknowledges your
162 work stoppage, computer failure or malfunction, or any and all162rights of fair use or other equivalent, as provided by copyright law.
163 other commercial damages or losses), even if such Contributor163
164 has been advised of the possibility of such damages.164 You may make, run and propagate covered works that you do not
165165convey, without conditions so long as your license otherwise remains
166 9. Accepting Warranty or Additional Liability. While redistributing166in force. You may convey covered works to others for the sole purpose
167 the Work or Derivative Works thereof, You may choose to offer,167of having them make modifications exclusively for you, or provide you
168 and charge a fee for, acceptance of support, warranty, indemnity,168with facilities for running those works, provided that you comply with
169 or other liability obligations and/or rights consistent with this169the terms of this License in conveying all material for which you do
170 License. However, in accepting such obligations, You may act only170not control copyright. Those thus making or running the covered works
171 on Your own behalf and on Your sole responsibility, not on behalf171for you must do so exclusively on your behalf, under your direction
172 of any other Contributor, and only if You agree to indemnify,172and control, on terms that prohibit them from making any copies of
173 defend, and hold each Contributor harmless for any liability173your copyrighted material outside their relationship with you.
174 incurred by, or claims asserted against, such Contributor by reason174
175 of your accepting any such warranty or additional liability.175 Conveying under any other circumstances is permitted solely under
176176the conditions stated below. Sublicensing is not allowed; section 10
177 END OF TERMS AND CONDITIONS177makes it unnecessary.
178178
179 APPENDIX: How to apply the Apache License to your work.179 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180180
181 To apply the Apache License to your work, attach the following181 No covered work shall be deemed part of an effective technological
182 boilerplate notice, with the fields enclosed by brackets "[]"182measure under any applicable law fulfilling obligations under article
183 replaced with your own identifying information. (Don't include18311 of the WIPO copyright treaty adopted on 20 December 1996, or
184 the brackets!) The text should be enclosed in the appropriate184similar laws prohibiting or restricting circumvention of such
185 comment syntax for the file format. We also recommend that a185measures.
186 file or class name and description of purpose be included on the186
187 same "printed page" as the copyright notice for easier187 When you convey a covered work, you waive any legal power to forbid
188 identification within third-party archives.188circumvention of technological measures to the extent such circumvention
189189is effected by exercising rights under this License with respect to
190 Copyright [yyyy] [name of copyright owner]190the covered work, and you disclaim any intention to limit operation or
191191modification of the work as a means of enforcing, against the work's
192 Licensed under the Apache License, Version 2.0 (the "License");192users, your or third parties' legal rights to forbid circumvention of
193 you may not use this file except in compliance with the License.193technological measures.
194 You may obtain a copy of the License at194
195195 4. Conveying Verbatim Copies.
196 http://www.apache.org/licenses/LICENSE-2.0196
197197 You may convey verbatim copies of the Program's source code as you
198 Unless required by applicable law or agreed to in writing, software198receive it, in any medium, provided that you conspicuously and
199 distributed under the License is distributed on an "AS IS" BASIS,199appropriately publish on each copy an appropriate copyright notice;
200 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.200keep intact all notices stating that this License and any
201 See the License for the specific language governing permissions and201non-permissive terms added in accord with section 7 apply to the code;
202 limitations under the License.202keep intact all notices of the absence of any warranty; and give all
203recipients a copy of this License along with the Program.
204
205 You may charge any price or no price for each copy that you convey,
206and you may offer support or warranty protection for a fee.
207
208 5. Conveying Modified Source Versions.
209
210 You may convey a work based on the Program, or the modifications to
211produce it from the Program, in the form of source code under the
212terms of section 4, provided that you also meet all of these conditions:
213
214 a) The work must carry prominent notices stating that you modified
215 it, and giving a relevant date.
216
217 b) The work must carry prominent notices stating that it is
218 released under this License and any conditions added under section
219 7. This requirement modifies the requirement in section 4 to
220 "keep intact all notices".
221
222 c) You must license the entire work, as a whole, under this
223 License to anyone who comes into possession of a copy. This
224 License will therefore apply, along with any applicable section 7
225 additional terms, to the whole of the work, and all its parts,
226 regardless of how they are packaged. This License gives no
227 permission to license the work in any other way, but it does not
228 invalidate such permission if you have separately received it.
229
230 d) If the work has interactive user interfaces, each must display
231 Appropriate Legal Notices; however, if the Program has interactive
232 interfaces that do not display Appropriate Legal Notices, your
233 work need not make them do so.
234
235 A compilation of a covered work with other separate and independent
236works, which are not by their nature extensions of the covered work,
237and which are not combined with it such as to form a larger program,
238in or on a volume of a storage or distribution medium, is called an
239"aggregate" if the compilation and its resulting copyright are not
240used to limit the access or legal rights of the compilation's users
241beyond what the individual works permit. Inclusion of a covered work
242in an aggregate does not cause this License to apply to the other
243parts of the aggregate.
244
245 6. Conveying Non-Source Forms.
246
247 You may convey a covered work in object code form under the terms
248of sections 4 and 5, provided that you also convey the
249machine-readable Corresponding Source under the terms of this License,
250in one of these ways:
251
252 a) Convey the object code in, or embodied in, a physical product
253 (including a physical distribution medium), accompanied by the
254 Corresponding Source fixed on a durable physical medium
255 customarily used for software interchange.
256
257 b) Convey the object code in, or embodied in, a physical product
258 (including a physical distribution medium), accompanied by a
259 written offer, valid for at least three years and valid for as
260 long as you offer spare parts or customer support for that product
261 model, to give anyone who possesses the object code either (1) a
262 copy of the Corresponding Source for all the software in the
263 product that is covered by this License, on a durable physical
264 medium customarily used for software interchange, for a price no
265 more than your reasonable cost of physically performing this
266 conveying of source, or (2) access to copy the
267 Corresponding Source from a network server at no charge.
268
269 c) Convey individual copies of the object code with a copy of the
270 written offer to provide the Corresponding Source. This
271 alternative is allowed only occasionally and noncommercially, and
272 only if you received the object code with such an offer, in accord
273 with subsection 6b.
274
275 d) Convey the object code by offering access from a designated
276 place (gratis or for a charge), and offer equivalent access to the
277 Corresponding Source in the same way through the same place at no
278 further charge. You need not require recipients to copy the
279 Corresponding Source along with the object code. If the place to
280 copy the object code is a network server, the Corresponding Source
281 may be on a different server (operated by you or a third party)
282 that supports equivalent copying facilities, provided you maintain
283 clear directions next to the object code saying where to find the
284 Corresponding Source. Regardless of what server hosts the
285 Corresponding Source, you remain obligated to ensure that it is
286 available for as long as needed to satisfy these requirements.
287
288 e) Convey the object code using peer-to-peer transmission, provided
289 you inform other peers where the object code and Corresponding
290 Source of the work are being offered to the general public at no
291 charge under subsection 6d.
292
293 A separable portion of the object code, whose source code is excluded
294from the Corresponding Source as a System Library, need not be
295included in conveying the object code work.
296
297 A "User Product" is either (1) a "consumer product", which means any
298tangible personal property which is normally used for personal, family,
299or household purposes, or (2) anything designed or sold for incorporation
300into a dwelling. In determining whether a product is a consumer product,
301doubtful cases shall be resolved in favor of coverage. For a particular
302product received by a particular user, "normally used" refers to a
303typical or common use of that class of product, regardless of the status
304of the particular user or of the way in which the particular user
305actually uses, or expects or is expected to use, the product. A product
306is a consumer product regardless of whether the product has substantial
307commercial, industrial or non-consumer uses, unless such uses represent
308the only significant mode of use of the product.
309
310 "Installation Information" for a User Product means any methods,
311procedures, authorization keys, or other information required to install
312and execute modified versions of a covered work in that User Product from
313a modified version of its Corresponding Source. The information must
314suffice to ensure that the continued functioning of the modified object
315code is in no case prevented or interfered with solely because
316modification has been made.
317
318 If you convey an object code work under this section in, or with, or
319specifically for use in, a User Product, and the conveying occurs as
320part of a transaction in which the right of possession and use of the
321User Product is transferred to the recipient in perpetuity or for a
322fixed term (regardless of how the transaction is characterized), the
323Corresponding Source conveyed under this section must be accompanied
324by the Installation Information. But this requirement does not apply
325if neither you nor any third party retains the ability to install
326modified object code on the User Product (for example, the work has
327been installed in ROM).
328
329 The requirement to provide Installation Information does not include a
330requirement to continue to provide support service, warranty, or updates
331for a work that has been modified or installed by the recipient, or for
332the User Product in which it has been modified or installed. Access to a
333network may be denied when the modification itself materially and
334adversely affects the operation of the network or violates the rules and
335protocols for communication across the network.
336
337 Corresponding Source conveyed, and Installation Information provided,
338in accord with this section must be in a format that is publicly
339documented (and with an implementation available to the public in
340source code form), and must require no special password or key for
341unpacking, reading or copying.
342
343 7. Additional Terms.
344
345 "Additional permissions" are terms that supplement the terms of this
346License by making exceptions from one or more of its conditions.
347Additional permissions that are applicable to the entire Program shall
348be treated as though they were included in this License, to the extent
349that they are valid under applicable law. If additional permissions
350apply only to part of the Program, that part may be used separately
351under those permissions, but the entire Program remains governed by
352this License without regard to the additional permissions.
353
354 When you convey a copy of a covered work, you may at your option
355remove any additional permissions from that copy, or from any part of
356it. (Additional permissions may be written to require their own
357removal in certain cases when you modify the work.) You may place
358additional permissions on material, added by you to a covered work,
359for which you have or can give appropriate copyright permission.
360
361 Notwithstanding any other provision of this License, for material you
362add to a covered work, you may (if authorized by the copyright holders of
363that material) supplement the terms of this License with terms:
364
365 a) Disclaiming warranty or limiting liability differently from the
366 terms of sections 15 and 16 of this License; or
367
368 b) Requiring preservation of specified reasonable legal notices or
369 author attributions in that material or in the Appropriate Legal
370 Notices displayed by works containing it; or
371
372 c) Prohibiting misrepresentation of the origin of that material, or
373 requiring that modified versions of such material be marked in
374 reasonable ways as different from the original version; or
375
376 d) Limiting the use for publicity purposes of names of licensors or
377 authors of the material; or
378
379 e) Declining to grant rights under trademark law for use of some
380 trade names, trademarks, or service marks; or
381
382 f) Requiring indemnification of licensors and authors of that
383 material by anyone who conveys the material (or modified versions of
384 it) with contractual assumptions of liability to the recipient, for
385 any liability that these contractual assumptions directly impose on
386 those licensors and authors.
387
388 All other non-permissive additional terms are considered "further
389restrictions" within the meaning of section 10. If the Program as you
390received it, or any part of it, contains a notice stating that it is
391governed by this License along with a term that is a further
392restriction, you may remove that term. If a license document contains
393a further restriction but permits relicensing or conveying under this
394License, you may add to a covered work material governed by the terms
395of that license document, provided that the further restriction does
396not survive such relicensing or conveying.
397
398 If you add terms to a covered work in accord with this section, you
399must place, in the relevant source files, a statement of the
400additional terms that apply to those files, or a notice indicating
401where to find the applicable terms.
402
403 Additional terms, permissive or non-permissive, may be stated in the
404form of a separately written license, or stated as exceptions;
405the above requirements apply either way.
406
407 8. Termination.
408
409 You may not propagate or modify a covered work except as expressly
410provided under this License. Any attempt otherwise to propagate or
411modify it is void, and will automatically terminate your rights under
412this License (including any patent licenses granted under the third
413paragraph of section 11).
414
415 However, if you cease all violation of this License, then your
416license from a particular copyright holder is reinstated (a)
417provisionally, unless and until the copyright holder explicitly and
418finally terminates your license, and (b) permanently, if the copyright
419holder fails to notify you of the violation by some reasonable means
420prior to 60 days after the cessation.
421
422 Moreover, your license from a particular copyright holder is
423reinstated permanently if the copyright holder notifies you of the
424violation by some reasonable means, this is the first time you have
425received notice of violation of this License (for any work) from that
426copyright holder, and you cure the violation prior to 30 days after
427your receipt of the notice.
428
429 Termination of your rights under this section does not terminate the
430licenses of parties who have received copies or rights from you under
431this License. If your rights have been terminated and not permanently
432reinstated, you do not qualify to receive new licenses for the same
433material under section 10.
434
435 9. Acceptance Not Required for Having Copies.
436
437 You are not required to accept this License in order to receive or
438run a copy of the Program. Ancillary propagation of a covered work
439occurring solely as a consequence of using peer-to-peer transmission
440to receive a copy likewise does not require acceptance. However,
441nothing other than this License grants you permission to propagate or
442modify any covered work. These actions infringe copyright if you do
443not accept this License. Therefore, by modifying or propagating a
444covered work, you indicate your acceptance of this License to do so.
445
446 10. Automatic Licensing of Downstream Recipients.
447
448 Each time you convey a covered work, the recipient automatically
449receives a license from the original licensors, to run, modify and
450propagate that work, subject to this License. You are not responsible
451for enforcing compliance by third parties with this License.
452
453 An "entity transaction" is a transaction transferring control of an
454organization, or substantially all assets of one, or subdividing an
455organization, or merging organizations. If propagation of a covered
456work results from an entity transaction, each party to that
457transaction who receives a copy of the work also receives whatever
458licenses to the work the party's predecessor in interest had or could
459give under the previous paragraph, plus a right to possession of the
460Corresponding Source of the work from the predecessor in interest, if
461the predecessor has it or can get it with reasonable efforts.
462
463 You may not impose any further restrictions on the exercise of the
464rights granted or affirmed under this License. For example, you may
465not impose a license fee, royalty, or other charge for exercise of
466rights granted under this License, and you may not initiate litigation
467(including a cross-claim or counterclaim in a lawsuit) alleging that
468any patent claim is infringed by making, using, selling, offering for
469sale, or importing the Program or any portion of it.
470
471 11. Patents.
472
473 A "contributor" is a copyright holder who authorizes use under this
474License of the Program or a work on which the Program is based. The
475work thus licensed is called the contributor's "contributor version".
476
477 A contributor's "essential patent claims" are all patent claims
478owned or controlled by the contributor, whether already acquired or
479hereafter acquired, that would be infringed by some manner, permitted
480by this License, of making, using, or selling its contributor version,
481but do not include claims that would be infringed only as a
482consequence of further modification of the contributor version. For
483purposes of this definition, "control" includes the right to grant
484patent sublicenses in a manner consistent with the requirements of
485this License.
486
487 Each contributor grants you a non-exclusive, worldwide, royalty-free
488patent license under the contributor's essential patent claims, to
489make, use, sell, offer for sale, import and otherwise run, modify and
490propagate the contents of its contributor version.
491
492 In the following three paragraphs, a "patent license" is any express
493agreement or commitment, however denominated, not to enforce a patent
494(such as an express permission to practice a patent or covenant not to
495sue for patent infringement). To "grant" such a patent license to a
496party means to make such an agreement or commitment not to enforce a
497patent against the party.
498
499 If you convey a covered work, knowingly relying on a patent license,
500and the Corresponding Source of the work is not available for anyone
501to copy, free of charge and under the terms of this License, through a
502publicly available network server or other readily accessible means,
503then you must either (1) cause the Corresponding Source to be so
504available, or (2) arrange to deprive yourself of the benefit of the
505patent license for this particular work, or (3) arrange, in a manner
506consistent with the requirements of this License, to extend the patent
507license to downstream recipients. "Knowingly relying" means you have
508actual knowledge that, but for the patent license, your conveying the
509covered work in a country, or your recipient's use of the covered work
510in a country, would infringe one or more identifiable patents in that
511country that you have reason to believe are valid.
512
513 If, pursuant to or in connection with a single transaction or
514arrangement, you convey, or propagate by procuring conveyance of, a
515covered work, and grant a patent license to some of the parties
516receiving the covered work authorizing them to use, propagate, modify
517or convey a specific copy of the covered work, then the patent license
518you grant is automatically extended to all recipients of the covered
519work and works based on it.
520
521 A patent license is "discriminatory" if it does not include within
522the scope of its coverage, prohibits the exercise of, or is
523conditioned on the non-exercise of one or more of the rights that are
524specifically granted under this License. You may not convey a covered
525work if you are a party to an arrangement with a third party that is
526in the business of distributing software, under which you make payment
527to the third party based on the extent of your activity of conveying
528the work, and under which the third party grants, to any of the
529parties who would receive the covered work from you, a discriminatory
530patent license (a) in connection with copies of the covered work
531conveyed by you (or copies made from those copies), or (b) primarily
532for and in connection with specific products or compilations that
533contain the covered work, unless you entered into that arrangement,
534or that patent license was granted, prior to 28 March 2007.
535
536 Nothing in this License shall be construed as excluding or limiting
537any implied license or other defenses to infringement that may
538otherwise be available to you under applicable patent law.
539
540 12. No Surrender of Others' Freedom.
541
542 If conditions are imposed on you (whether by court order, agreement or
543otherwise) that contradict the conditions of this License, they do not
544excuse you from the conditions of this License. If you cannot convey a
545covered work so as to satisfy simultaneously your obligations under this
546License and any other pertinent obligations, then as a consequence you may
547not convey it at all. For example, if you agree to terms that obligate you
548to collect a royalty for further conveying from those to whom you convey
549the Program, the only way you could satisfy both those terms and this
550License would be to refrain entirely from conveying the Program.
551
552 13. Use with the GNU Affero General Public License.
553
554 Notwithstanding any other provision of this License, you have
555permission to link or combine any covered work with a work licensed
556under version 3 of the GNU Affero General Public License into a single
557combined work, and to convey the resulting work. The terms of this
558License will continue to apply to the part which is the covered work,
559but the special requirements of the GNU Affero General Public License,
560section 13, concerning interaction through a network will apply to the
561combination as such.
562
563 14. Revised Versions of this License.
564
565 The Free Software Foundation may publish revised and/or new versions of
566the GNU General Public License from time to time. Such new versions will
567be similar in spirit to the present version, but may differ in detail to
568address new problems or concerns.
569
570 Each version is given a distinguishing version number. If the
571Program specifies that a certain numbered version of the GNU General
572Public License "or any later version" applies to it, you have the
573option of following the terms and conditions either of that numbered
574version or of any later version published by the Free Software
575Foundation. If the Program does not specify a version number of the
576GNU General Public License, you may choose any version ever published
577by the Free Software Foundation.
578
579 If the Program specifies that a proxy can decide which future
580versions of the GNU General Public License can be used, that proxy's
581public statement of acceptance of a version permanently authorizes you
582to choose that version for the Program.
583
584 Later license versions may give you additional or different
585permissions. However, no additional obligations are imposed on any
586author or copyright holder as a result of your choosing to follow a
587later version.
588
589 15. Disclaimer of Warranty.
590
591 THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599
600 16. Limitation of Liability.
601
602 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610SUCH DAMAGES.
611
612 17. Interpretation of Sections 15 and 16.
613
614 If the disclaimer of warranty and limitation of liability provided
615above cannot be given local legal effect according to their terms,
616reviewing courts shall apply local law that most closely approximates
617an absolute waiver of all civil liability in connection with the
618Program, unless a warranty or assumption of liability accompanies a
619copy of the Program in return for a fee.
620
621 END OF TERMS AND CONDITIONS
622
623 How to Apply These Terms to Your New Programs
624
625 If you develop a new program, and you want it to be of the greatest
626possible use to the public, the best way to achieve this is to make it
627free software which everyone can redistribute and change under these terms.
628
629 To do so, attach the following notices to the program. It is safest
630to attach them to the start of each source file to most effectively
631state the exclusion of warranty; and each file should have at least
632the "copyright" line and a pointer to where the full notice is found.
633
634 <one line to give the program's name and a brief idea of what it does.>
635 Copyright (C) <year> <name of author>
636
637 This program is free software: you can redistribute it and/or modify
638 it under the terms of the GNU General Public License as published by
639 the Free Software Foundation, either version 3 of the License, or
640 (at your option) any later version.
641
642 This program is distributed in the hope that it will be useful,
643 but WITHOUT ANY WARRANTY; without even the implied warranty of
644 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 GNU General Public License for more details.
646
647 You should have received a copy of the GNU General Public License
648 along with this program. If not, see <https://www.gnu.org/licenses/>.
649
650Also add information on how to contact you by electronic and paper mail.
651
652 If the program does terminal interaction, make it output a short
653notice like this when it starts in an interactive mode:
654
655 <program> Copyright (C) <year> <name of author>
656 This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 This is free software, and you are welcome to redistribute it
658 under certain conditions; type `show c' for details.
659
660The hypothetical commands `show w' and `show c' should show the appropriate
661parts of the General Public License. Of course, your program's commands
662might be different; for a GUI interface, you would use an "about box".
663
664 You should also get your employer (if you work as a programmer) or school,
665if any, to sign a "copyright disclaimer" for the program, if necessary.
666For more information on this, and how to apply and follow the GNU GPL, see
667<https://www.gnu.org/licenses/>.
668
669 The GNU General Public License does not permit incorporating your program
670into proprietary programs. If your program is a subroutine library, you
671may consider it more useful to permit linking proprietary applications with
672the library. If this is what you want to do, use the GNU Lesser General
673Public License instead of this License. But first, please read
674<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/Makefile b/Makefile
203new file mode 100644675new file mode 100644
index 0000000..8c14478
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,21 @@
1METADATA_FILE="metadata.yaml"
2PROJECTPATH=$(dir $(realpath $(MAKEFILE_LIST)))
3
4ifndef CHARM_BUILD_DIR
5 CHARM_BUILD_DIR=${PROJECTPATH}/../builds
6endif
7
8CHARM_NAME=$(shell cat ${PROJECTPATH}/${METADATA_FILE} | grep -E "^name:" | awk '{print $$2}')
9
10clean:
11 @echo "Cleaning files"
12 @git clean -ffXd -e '!.idea'
13 @echo "Cleaning existing build"
14 @rm -rf ${CHARM_BUILD_DIR}/${CHARM_NAME}
15
16lint:
17 @echo "Running lint checks"
18 @cd src && tox -e lint
19
20# The targets below don't depend on a file
21.PHONY: clean lint
diff --git a/README.md b/README.md
index 148c70d..a53a7ed 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,16 @@
1# charm-tor1# charm-tor-hidden-service
22
3## Description3## Description
44
5TODO: Describe your charm in a few paragraphs of Markdown5This charm enables Tor hidden services
66
7## Usage7## Usage
88
9TODO: Provide high-level usage, such as required config or relations9The charm relates to any number of services (e.g. ch:apache2) over the
10`reverseproxy` relation, and serves up a proxied version of that site on a
11.onion (v3) hostname.
1012
13If you require a specific .onion hostname (or multiple) we suggest using the
14mkp224o tool to create them, and configuring the charm with those private keys.
1115
12## Relations16TODO
13
14TODO: Provide any relations which are provided or required by your charm
15
16## OCI Images
17
18TODO: Include a link to the default image your charm uses
19
20## Contributing
21
22Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines
23on enhancements to this charm following best practice guidelines, and
24`CONTRIBUTING.md` for developer guidance.
diff --git a/actions.yaml b/actions.yaml
25deleted file mode 10064417deleted file mode 100644
index 7cda621..0000000
--- a/actions.yaml
+++ /dev/null
@@ -1,16 +0,0 @@
1# Copyright 2022 Barry Price
2# See LICENSE file for licensing details.
3#
4# TEMPLATE-TODO: change this example to suit your needs.
5# If you don't need actions, you can remove the file entirely.
6# It ties in to the example _on_fortune_action handler in src/charm.py
7#
8# Learn more about actions at: https://juju.is/docs/sdk/actions
9
10fortune:
11 description: Returns a pithy phrase.
12 params:
13 fail:
14 description: "Fail with this message"
15 type: string
16 default: ""
diff --git a/charmcraft.yaml b/charmcraft.yaml
index 048d454..0d1497e 100644
--- a/charmcraft.yaml
+++ b/charmcraft.yaml
@@ -1,10 +1,10 @@
1# Learn more about charmcraft.yaml configuration at:
2# https://juju.is/docs/sdk/charmcraft-config
3type: "charm"1type: "charm"
4bases:2bases:
5 - build-on:3 - build-on:
6 - name: "ubuntu"4 - name: "ubuntu"
7 channel: "20.04"5 channel: "22.04"
8 run-on:6 run-on:
9 - name: "ubuntu"7 - name: "ubuntu"
10 channel: "20.04"8 channel: "20.04"
9 - name: "ubuntu"
10 channel: "22.04"
diff --git a/config.yaml b/config.yaml
index 65fb1ce..35643c0 100644
--- a/config.yaml
+++ b/config.yaml
@@ -1,14 +1,33 @@
1# Copyright 2022 Barry Price
2# See LICENSE file for licensing details.
3#
4# TEMPLATE-TODO: change this example to suit your needs.
5# If you don't need a config, you can remove the file entirely.
6# It ties in to the example _on_config_changed handler in src/charm.py
7#
8# Learn more about config at: https://juju.is/docs/sdk/config
9
10options:1options:
11 thing:2 hidden_keys_base64:
12 default: 🎁3 default: ""
13 description: A thing used by the charm.4 description: |
5 Optional pre-generated tor private keys to run your service. The
6 .onion hostname(s) will be derived from this value.
7
8 Since the key is a binary file, this field expects a YAML formattaed
9 list of source hostnames (passed via the reverseproxy relation and
10 visible in the Message column via `juju status`) with corresponding
11 base-64 encoded strings, via e.g. `base64 -w0 path/to/secretkey`.
12
13 If left unconfigured, a random key (and address) will be generated for
14 each site.
15
16 e.g.
17 example1.com: ZXhhbXBsZTEK
18 example2.internal: ZXhhbXBsZTIK
19 10.0.0.1: ZXhhbXBsZTMK # example3
20 type: string
21 socks5_port:
22 description: SOCKS5 proxy port on which to listen
23 type: int
24 default: 9050
25 tor_source:
26 default: torproject
27 description: |
28 Source from which tor will be installed.
29 Options are "torproject" (default), which will use the packages from
30 deb.torproject.org, or "ubuntu" which will use the package in ubuntu's
31 universe component (this option may be outdated/insecure, but is the
32 only current option for architectures other than amd64/arm64).
14 type: string33 type: string
diff --git a/lib/charms/operator_libs_linux/v0/apt.py b/lib/charms/operator_libs_linux/v0/apt.py
15new file mode 10064434new file mode 100644
index 0000000..2b5c8f2
--- /dev/null
+++ b/lib/charms/operator_libs_linux/v0/apt.py
@@ -0,0 +1,1329 @@
1# Copyright 2021 Canonical Ltd.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Abstractions for the system's Debian/Ubuntu package information and repositories.
16
17This module contains abstractions and wrappers around Debian/Ubuntu-style repositories and
18packages, in order to easily provide an idiomatic and Pythonic mechanism for adding packages and/or
19repositories to systems for use in machine charms.
20
21A sane default configuration is attainable through nothing more than instantiation of the
22appropriate classes. `DebianPackage` objects provide information about the architecture, version,
23name, and status of a package.
24
25`DebianPackage` will try to look up a package either from `dpkg -L` or from `apt-cache` when
26provided with a string indicating the package name. If it cannot be located, `PackageNotFoundError`
27will be returned, as `apt` and `dpkg` otherwise return `100` for all errors, and a meaningful error
28message if the package is not known is desirable.
29
30To install packages with convenience methods:
31
32```python
33try:
34 # Run `apt-get update`
35 apt.update()
36 apt.add_package("zsh")
37 apt.add_package(["vim", "htop", "wget"])
38except PackageNotFoundError:
39 logger.error("a specified package not found in package cache or on system")
40except PackageError as e:
41 logger.error("could not install package. Reason: %s", e.message)
42````
43
44To find details of a specific package:
45
46```python
47try:
48 vim = apt.DebianPackage.from_system("vim")
49
50 # To find from the apt cache only
51 # apt.DebianPackage.from_apt_cache("vim")
52
53 # To find from installed packages only
54 # apt.DebianPackage.from_installed_package("vim")
55
56 vim.ensure(PackageState.Latest)
57 logger.info("updated vim to version: %s", vim.fullversion)
58except PackageNotFoundError:
59 logger.error("a specified package not found in package cache or on system")
60except PackageError as e:
61 logger.error("could not install package. Reason: %s", e.message)
62```
63
64
65`RepositoryMapping` will return a dict-like object containing enabled system repositories
66and their properties (available groups, baseuri. gpg key). This class can add, disable, or
67manipulate repositories. Items can be retrieved as `DebianRepository` objects.
68
69In order add a new repository with explicit details for fields, a new `DebianRepository` can
70be added to `RepositoryMapping`
71
72`RepositoryMapping` provides an abstraction around the existing repositories on the system,
73and can be accessed and iterated over like any `Mapping` object, to retrieve values by key,
74iterate, or perform other operations.
75
76Keys are constructed as `{repo_type}-{}-{release}` in order to uniquely identify a repository.
77
78Repositories can be added with explicit values through a Python constructor.
79
80Example:
81
82```python
83repositories = apt.RepositoryMapping()
84
85if "deb-example.com-focal" not in repositories:
86 repositories.add(DebianRepository(enabled=True, repotype="deb",
87 uri="https://example.com", release="focal", groups=["universe"]))
88```
89
90Alternatively, any valid `sources.list` line may be used to construct a new
91`DebianRepository`.
92
93Example:
94
95```python
96repositories = apt.RepositoryMapping()
97
98if "deb-us.archive.ubuntu.com-xenial" not in repositories:
99 line = "deb http://us.archive.ubuntu.com/ubuntu xenial main restricted"
100 repo = DebianRepository.from_repo_line(line)
101 repositories.add(repo)
102```
103"""
104
105import fileinput
106import glob
107import logging
108import os
109import re
110import subprocess
111from collections.abc import Mapping
112from enum import Enum
113from subprocess import PIPE, CalledProcessError, check_call, check_output
114from typing import Iterable, List, Optional, Tuple, Union
115from urllib.parse import urlparse
116
117logger = logging.getLogger(__name__)
118
119# The unique Charmhub library identifier, never change it
120LIBID = "7c3dbc9c2ad44a47bd6fcb25caa270e5"
121
122# Increment this major API version when introducing breaking changes
123LIBAPI = 0
124
125# Increment this PATCH version before using `charmcraft publish-lib` or reset
126# to 0 if you are raising the major API version
127LIBPATCH = 7
128
129
130VALID_SOURCE_TYPES = ("deb", "deb-src")
131OPTIONS_MATCHER = re.compile(r"\[.*?\]")
132
133
134class Error(Exception):
135 """Base class of most errors raised by this library."""
136
137 def __repr__(self):
138 """String representation of Error."""
139 return "<{}.{} {}>".format(type(self).__module__, type(self).__name__, self.args)
140
141 @property
142 def name(self):
143 """Return a string representation of the model plus class."""
144 return "<{}.{}>".format(type(self).__module__, type(self).__name__)
145
146 @property
147 def message(self):
148 """Return the message passed as an argument."""
149 return self.args[0]
150
151
152class PackageError(Error):
153 """Raised when there's an error installing or removing a package."""
154
155
156class PackageNotFoundError(Error):
157 """Raised when a requested package is not known to the system."""
158
159
160class PackageState(Enum):
161 """A class to represent possible package states."""
162
163 Present = "present"
164 Absent = "absent"
165 Latest = "latest"
166 Available = "available"
167
168
169class DebianPackage:
170 """Represents a traditional Debian package and its utility functions.
171
172 `DebianPackage` wraps information and functionality around a known package, whether installed
173 or available. The version, epoch, name, and architecture can be easily queried and compared
174 against other `DebianPackage` objects to determine the latest version or to install a specific
175 version.
176
177 The representation of this object as a string mimics the output from `dpkg` for familiarity.
178
179 Installation and removal of packages is handled through the `state` property or `ensure`
180 method, with the following options:
181
182 apt.PackageState.Absent
183 apt.PackageState.Available
184 apt.PackageState.Present
185 apt.PackageState.Latest
186
187 When `DebianPackage` is initialized, the state of a given `DebianPackage` object will be set to
188 `Available`, `Present`, or `Latest`, with `Absent` implemented as a convenience for removal
189 (though it operates essentially the same as `Available`).
190 """
191
192 def __init__(
193 self, name: str, version: str, epoch: str, arch: str, state: PackageState
194 ) -> None:
195 self._name = name
196 self._arch = arch
197 self._state = state
198 self._version = Version(version, epoch)
199
200 def __eq__(self, other) -> bool:
201 """Equality for comparison.
202
203 Args:
204 other: a `DebianPackage` object for comparison
205
206 Returns:
207 A boolean reflecting equality
208 """
209 return isinstance(other, self.__class__) and (
210 self._name,
211 self._version.number,
212 ) == (other._name, other._version.number)
213
214 def __hash__(self):
215 """A basic hash so this class can be used in Mappings and dicts."""
216 return hash((self._name, self._version.number))
217
218 def __repr__(self):
219 """A representation of the package."""
220 return "<{}.{}: {}>".format(self.__module__, self.__class__.__name__, self.__dict__)
221
222 def __str__(self):
223 """A human-readable representation of the package."""
224 return "<{}: {}-{}.{} -- {}>".format(
225 self.__class__.__name__,
226 self._name,
227 self._version,
228 self._arch,
229 str(self._state),
230 )
231
232 @staticmethod
233 def _apt(
234 command: str,
235 package_names: Union[str, List],
236 optargs: Optional[List[str]] = None,
237 ) -> None:
238 """Wrap package management commands for Debian/Ubuntu systems.
239
240 Args:
241 command: the command given to `apt-get`
242 package_names: a package name or list of package names to operate on
243 optargs: an (Optional) list of additioanl arguments
244
245 Raises:
246 PackageError if an error is encountered
247 """
248 optargs = optargs if optargs is not None else []
249 if isinstance(package_names, str):
250 package_names = [package_names]
251 _cmd = ["apt-get", "-y", *optargs, command, *package_names]
252 try:
253 check_call(_cmd, stderr=PIPE, stdout=PIPE)
254 except CalledProcessError as e:
255 raise PackageError(
256 "Could not {} package(s) [{}]: {}".format(command, [*package_names], e.output)
257 ) from None
258
259 def _add(self) -> None:
260 """Add a package to the system."""
261 self._apt(
262 "install",
263 "{}={}".format(self.name, self.version),
264 optargs=["--option=Dpkg::Options::=--force-confold"],
265 )
266
267 def _remove(self) -> None:
268 """Removes a package from the system. Implementation-specific."""
269 return self._apt("remove", "{}={}".format(self.name, self.version))
270
271 @property
272 def name(self) -> str:
273 """Returns the name of the package."""
274 return self._name
275
276 def ensure(self, state: PackageState):
277 """Ensures that a package is in a given state.
278
279 Args:
280 state: a `PackageState` to reconcile the package to
281
282 Raises:
283 PackageError from the underlying call to apt
284 """
285 if self._state is not state:
286 if state not in (PackageState.Present, PackageState.Latest):
287 self._remove()
288 else:
289 self._add()
290 self._state = state
291
292 @property
293 def present(self) -> bool:
294 """Returns whether or not a package is present."""
295 return self._state in (PackageState.Present, PackageState.Latest)
296
297 @property
298 def latest(self) -> bool:
299 """Returns whether the package is the most recent version."""
300 return self._state is PackageState.Latest
301
302 @property
303 def state(self) -> PackageState:
304 """Returns the current package state."""
305 return self._state
306
307 @state.setter
308 def state(self, state: PackageState) -> None:
309 """Sets the package state to a given value.
310
311 Args:
312 state: a `PackageState` to reconcile the package to
313
314 Raises:
315 PackageError from the underlying call to apt
316 """
317 if state in (PackageState.Latest, PackageState.Present):
318 self._add()
319 else:
320 self._remove()
321 self._state = state
322
323 @property
324 def version(self) -> "Version":
325 """Returns the version for a package."""
326 return self._version
327
328 @property
329 def epoch(self) -> str:
330 """Returns the epoch for a package. May be unset."""
331 return self._version.epoch
332
333 @property
334 def arch(self) -> str:
335 """Returns the architecture for a package."""
336 return self._arch
337
338 @property
339 def fullversion(self) -> str:
340 """Returns the name+epoch for a package."""
341 return "{}.{}".format(self._version, self._arch)
342
343 @staticmethod
344 def _get_epoch_from_version(version: str) -> Tuple[str, str]:
345 """Pull the epoch, if any, out of a version string."""
346 epoch_matcher = re.compile(r"^((?P<epoch>\d+):)?(?P<version>.*)")
347 matches = epoch_matcher.search(version).groupdict()
348 return matches.get("epoch", ""), matches.get("version")
349
350 @classmethod
351 def from_system(
352 cls, package: str, version: Optional[str] = "", arch: Optional[str] = ""
353 ) -> "DebianPackage":
354 """Locates a package, either on the system or known to apt, and serializes the information.
355
356 Args:
357 package: a string representing the package
358 version: an optional string if a specific version isr equested
359 arch: an optional architecture, defaulting to `dpkg --print-architecture`. If an
360 architecture is not specified, this will be used for selection.
361
362 """
363 try:
364 return DebianPackage.from_installed_package(package, version, arch)
365 except PackageNotFoundError:
366 logger.debug(
367 "package '%s' is not currently installed or has the wrong architecture.", package
368 )
369
370 # Ok, try `apt-cache ...`
371 try:
372 return DebianPackage.from_apt_cache(package, version, arch)
373 except (PackageNotFoundError, PackageError):
374 # If we get here, it's not known to the systems.
375 # This seems unnecessary, but virtually all `apt` commands have a return code of `100`,
376 # and providing meaningful error messages without this is ugly.
377 raise PackageNotFoundError(
378 "Package '{}{}' could not be found on the system or in the apt cache!".format(
379 package, ".{}".format(arch) if arch else ""
380 )
381 ) from None
382
383 @classmethod
384 def from_installed_package(
385 cls, package: str, version: Optional[str] = "", arch: Optional[str] = ""
386 ) -> "DebianPackage":
387 """Check whether the package is already installed and return an instance.
388
389 Args:
390 package: a string representing the package
391 version: an optional string if a specific version isr equested
392 arch: an optional architecture, defaulting to `dpkg --print-architecture`.
393 If an architecture is not specified, this will be used for selection.
394 """
395 system_arch = check_output(
396 ["dpkg", "--print-architecture"], universal_newlines=True
397 ).strip()
398 arch = arch if arch else system_arch
399
400 # Regexps are a really terrible way to do this. Thanks dpkg
401 output = ""
402 try:
403 output = check_output(["dpkg", "-l", package], stderr=PIPE, universal_newlines=True)
404 except CalledProcessError:
405 raise PackageNotFoundError("Package is not installed: {}".format(package)) from None
406
407 # Pop off the output from `dpkg -l' because there's no flag to
408 # omit it`
409 lines = str(output).splitlines()[5:]
410
411 dpkg_matcher = re.compile(
412 r"""
413 ^(?P<package_status>\w+?)\s+
414 (?P<package_name>.*?)(?P<throwaway_arch>:\w+?)?\s+
415 (?P<version>.*?)\s+
416 (?P<arch>\w+?)\s+
417 (?P<description>.*)
418 """,
419 re.VERBOSE,
420 )
421
422 for line in lines:
423 try:
424 matches = dpkg_matcher.search(line).groupdict()
425 package_status = matches["package_status"]
426
427 if not package_status.endswith("i"):
428 logger.debug(
429 "package '%s' in dpkg output but not installed, status: '%s'",
430 package,
431 package_status,
432 )
433 break
434
435 epoch, split_version = DebianPackage._get_epoch_from_version(matches["version"])
436 pkg = DebianPackage(
437 matches["package_name"],
438 split_version,
439 epoch,
440 matches["arch"],
441 PackageState.Present,
442 )
443 if (pkg.arch == "all" or pkg.arch == arch) and (
444 version == "" or str(pkg.version) == version
445 ):
446 return pkg
447 except AttributeError:
448 logger.warning("dpkg matcher could not parse line: %s", line)
449
450 # If we didn't find it, fail through
451 raise PackageNotFoundError("Package {}.{} is not installed!".format(package, arch))
452
453 @classmethod
454 def from_apt_cache(
455 cls, package: str, version: Optional[str] = "", arch: Optional[str] = ""
456 ) -> "DebianPackage":
457 """Check whether the package is already installed and return an instance.
458
459 Args:
460 package: a string representing the package
461 version: an optional string if a specific version isr equested
462 arch: an optional architecture, defaulting to `dpkg --print-architecture`.
463 If an architecture is not specified, this will be used for selection.
464 """
465 system_arch = check_output(
466 ["dpkg", "--print-architecture"], universal_newlines=True
467 ).strip()
468 arch = arch if arch else system_arch
469
470 # Regexps are a really terrible way to do this. Thanks dpkg
471 keys = ("Package", "Architecture", "Version")
472
473 try:
474 output = check_output(
475 ["apt-cache", "show", package], stderr=PIPE, universal_newlines=True
476 )
477 except CalledProcessError as e:
478 raise PackageError(
479 "Could not list packages in apt-cache: {}".format(e.output)
480 ) from None
481
482 pkg_groups = output.strip().split("\n\n")
483 keys = ("Package", "Architecture", "Version")
484
485 for pkg_raw in pkg_groups:
486 lines = str(pkg_raw).splitlines()
487 vals = {}
488 for line in lines:
489 if line.startswith(keys):
490 items = line.split(":", 1)
491 vals[items[0]] = items[1].strip()
492 else:
493 continue
494
495 epoch, split_version = DebianPackage._get_epoch_from_version(vals["Version"])
496 pkg = DebianPackage(
497 vals["Package"],
498 split_version,
499 epoch,
500 vals["Architecture"],
501 PackageState.Available,
502 )
503
504 if (pkg.arch == "all" or pkg.arch == arch) and (
505 version == "" or str(pkg.version) == version
506 ):
507 return pkg
508
509 # If we didn't find it, fail through
510 raise PackageNotFoundError("Package {}.{} is not in the apt cache!".format(package, arch))
511
512
513class Version:
514 """An abstraction around package versions.
515
516 This seems like it should be strictly unnecessary, except that `apt_pkg` is not usable inside a
517 venv, and wedging version comparisions into `DebianPackage` would overcomplicate it.
518
519 This class implements the algorithm found here:
520 https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
521 """
522
523 def __init__(self, version: str, epoch: str):
524 self._version = version
525 self._epoch = epoch or ""
526
527 def __repr__(self):
528 """A representation of the package."""
529 return "<{}.{}: {}>".format(self.__module__, self.__class__.__name__, self.__dict__)
530
531 def __str__(self):
532 """A human-readable representation of the package."""
533 return "{}{}".format("{}:".format(self._epoch) if self._epoch else "", self._version)
534
535 @property
536 def epoch(self):
537 """Returns the epoch for a package. May be empty."""
538 return self._epoch
539
540 @property
541 def number(self) -> str:
542 """Returns the version number for a package."""
543 return self._version
544
545 def _get_parts(self, version: str) -> Tuple[str, str]:
546 """Separate the version into component upstream and Debian pieces."""
547 try:
548 version.rindex("-")
549 except ValueError:
550 # No hyphens means no Debian version
551 return version, "0"
552
553 upstream, debian = version.rsplit("-", 1)
554 return upstream, debian
555
556 def _listify(self, revision: str) -> List[str]:
557 """Split a revision string into a listself.
558
559 This list is comprised of alternating between strings and numbers,
560 padded on either end to always be "str, int, str, int..." and
561 always be of even length. This allows us to trivially implement the
562 comparison algorithm described.
563 """
564 result = []
565 while revision:
566 rev_1, remains = self._get_alphas(revision)
567 rev_2, remains = self._get_digits(remains)
568 result.extend([rev_1, rev_2])
569 revision = remains
570 return result
571
572 def _get_alphas(self, revision: str) -> Tuple[str, str]:
573 """Return a tuple of the first non-digit characters of a revision."""
574 # get the index of the first digit
575 for i, char in enumerate(revision):
576 if char.isdigit():
577 if i == 0:
578 return "", revision
579 return revision[0:i], revision[i:]
580 # string is entirely alphas
581 return revision, ""
582
583 def _get_digits(self, revision: str) -> Tuple[int, str]:
584 """Return a tuple of the first integer characters of a revision."""
585 # If the string is empty, return (0,'')
586 if not revision:
587 return 0, ""
588 # get the index of the first non-digit
589 for i, char in enumerate(revision):
590 if not char.isdigit():
591 if i == 0:
592 return 0, revision
593 return int(revision[0:i]), revision[i:]
594 # string is entirely digits
595 return int(revision), ""
596
597 def _dstringcmp(self, a, b): # noqa: C901
598 """Debian package version string section lexical sort algorithm.
599
600 The lexical comparison is a comparison of ASCII values modified so
601 that all the letters sort earlier than all the non-letters and so that
602 a tilde sorts before anything, even the end of a part.
603 """
604 if a == b:
605 return 0
606 try:
607 for i, char in enumerate(a):
608 if char == b[i]:
609 continue
610 # "a tilde sorts before anything, even the end of a part"
611 # (emptyness)
612 if char == "~":
613 return -1
614 if b[i] == "~":
615 return 1
616 # "all the letters sort earlier than all the non-letters"
617 if char.isalpha() and not b[i].isalpha():
618 return -1
619 if not char.isalpha() and b[i].isalpha():
620 return 1
621 # otherwise lexical sort
622 if ord(char) > ord(b[i]):
623 return 1
624 if ord(char) < ord(b[i]):
625 return -1
626 except IndexError:
627 # a is longer than b but otherwise equal, greater unless there are tildes
628 if char == "~":
629 return -1
630 return 1
631 # if we get here, a is shorter than b but otherwise equal, so check for tildes...
632 if b[len(a)] == "~":
633 return 1
634 return -1
635
636 def _compare_revision_strings(self, first: str, second: str): # noqa: C901
637 """Compare two debian revision strings."""
638 if first == second:
639 return 0
640
641 # listify pads results so that we will always be comparing ints to ints
642 # and strings to strings (at least until we fall off the end of a list)
643 first_list = self._listify(first)
644 second_list = self._listify(second)
645 if first_list == second_list:
646 return 0
647 try:
648 for i, item in enumerate(first_list):
649 # explicitly raise IndexError if we've fallen off the edge of list2
650 if i >= len(second_list):
651 raise IndexError
652 # if the items are equal, next
653 if item == second_list[i]:
654 continue
655 # numeric comparison
656 if isinstance(item, int):
657 if item > second_list[i]:
658 return 1
659 if item < second_list[i]:
660 return -1
661 else:
662 # string comparison
663 return self._dstringcmp(item, second_list[i])
664 except IndexError:
665 # rev1 is longer than rev2 but otherwise equal, hence greater
666 # ...except for goddamn tildes
667 if first_list[len(second_list)][0][0] == "~":
668 return 1
669 return 1
670 # rev1 is shorter than rev2 but otherwise equal, hence lesser
671 # ...except for goddamn tildes
672 if second_list[len(first_list)][0][0] == "~":
673 return -1
674 return -1
675
676 def _compare_version(self, other) -> int:
677 if (self.number, self.epoch) == (other.number, other.epoch):
678 return 0
679
680 if self.epoch < other.epoch:
681 return -1
682 if self.epoch > other.epoch:
683 return 1
684
685 # If none of these are true, follow the algorithm
686 upstream_version, debian_version = self._get_parts(self.number)
687 other_upstream_version, other_debian_version = self._get_parts(other.number)
688
689 upstream_cmp = self._compare_revision_strings(upstream_version, other_upstream_version)
690 if upstream_cmp != 0:
691 return upstream_cmp
692
693 debian_cmp = self._compare_revision_strings(debian_version, other_debian_version)
694 if debian_cmp != 0:
695 return debian_cmp
696
697 return 0
698
699 def __lt__(self, other) -> bool:
700 """Less than magic method impl."""
701 return self._compare_version(other) < 0
702
703 def __eq__(self, other) -> bool:
704 """Equality magic method impl."""
705 return self._compare_version(other) == 0
706
707 def __gt__(self, other) -> bool:
708 """Greater than magic method impl."""
709 return self._compare_version(other) > 0
710
711 def __le__(self, other) -> bool:
712 """Less than or equal to magic method impl."""
713 return self.__eq__(other) or self.__lt__(other)
714
715 def __ge__(self, other) -> bool:
716 """Greater than or equal to magic method impl."""
717 return self.__gt__(other) or self.__eq__(other)
718
719 def __ne__(self, other) -> bool:
720 """Not equal to magic method impl."""
721 return not self.__eq__(other)
722
723
724def add_package(
725 package_names: Union[str, List[str]],
726 version: Optional[str] = "",
727 arch: Optional[str] = "",
728 update_cache: Optional[bool] = False,
729) -> Union[DebianPackage, List[DebianPackage]]:
730 """Add a package or list of packages to the system.
731
732 Args:
733 name: the name(s) of the package(s)
734 version: an (Optional) version as a string. Defaults to the latest known
735 arch: an optional architecture for the package
736 update_cache: whether or not to run `apt-get update` prior to operating
737
738 Raises:
739 PackageNotFoundError if the package is not in the cache.
740 """
741 cache_refreshed = False
742 if update_cache:
743 update()
744 cache_refreshed = True
745
746 packages = {"success": [], "retry": [], "failed": []}
747
748 package_names = [package_names] if type(package_names) is str else package_names
749 if not package_names:
750 raise TypeError("Expected at least one package name to add, received zero!")
751
752 if len(package_names) != 1 and version:
753 raise TypeError(
754 "Explicit version should not be set if more than one package is being added!"
755 )
756
757 for p in package_names:
758 pkg, success = _add(p, version, arch)
759 if success:
760 packages["success"].append(pkg)
761 else:
762 logger.warning("failed to locate and install/update '%s'", pkg)
763 packages["retry"].append(p)
764
765 if packages["retry"] and not cache_refreshed:
766 logger.info("updating the apt-cache and retrying installation of failed packages.")
767 update()
768
769 for p in packages["retry"]:
770 pkg, success = _add(p, version, arch)
771 if success:
772 packages["success"].append(pkg)
773 else:
774 packages["failed"].append(p)
775
776 if packages["failed"]:
777 raise PackageError("Failed to install packages: {}".format(", ".join(packages["failed"])))
778
779 return packages["success"] if len(packages["success"]) > 1 else packages["success"][0]
780
781
782def _add(
783 name: str,
784 version: Optional[str] = "",
785 arch: Optional[str] = "",
786) -> Tuple[Union[DebianPackage, str], bool]:
787 """Adds a package.
788
789 Args:
790 name: the name(s) of the package(s)
791 version: an (Optional) version as a string. Defaults to the latest known
792 arch: an optional architecture for the package
793
794 Returns: a tuple of `DebianPackage` if found, or a :str: if it is not, and
795 a boolean indicating success
796 """
797 try:
798 pkg = DebianPackage.from_system(name, version, arch)
799 pkg.ensure(state=PackageState.Present)
800 return pkg, True
801 except PackageNotFoundError:
802 return name, False
803
804
805def remove_package(
806 package_names: Union[str, List[str]]
807) -> Union[DebianPackage, List[DebianPackage]]:
808 """Removes a package from the system.
809
810 Args:
811 package_names: the name of a package
812
813 Raises:
814 PackageNotFoundError if the package is not found.
815 """
816 packages = []
817
818 package_names = [package_names] if type(package_names) is str else package_names
819 if not package_names:
820 raise TypeError("Expected at least one package name to add, received zero!")
821
822 for p in package_names:
823 try:
824 pkg = DebianPackage.from_installed_package(p)
825 pkg.ensure(state=PackageState.Absent)
826 packages.append(pkg)
827 except PackageNotFoundError:
828 logger.info("package '%s' was requested for removal, but it was not installed.", p)
829
830 # the list of packages will be empty when no package is removed
831 logger.debug("packages: '%s'", packages)
832 return packages[0] if len(packages) == 1 else packages
833
834
835def update() -> None:
836 """Updates the apt cache via `apt-get update`."""
837 check_call(["apt-get", "update"], stderr=PIPE, stdout=PIPE)
838
839
840class InvalidSourceError(Error):
841 """Exceptions for invalid source entries."""
842
843
844class GPGKeyError(Error):
845 """Exceptions for GPG keys."""
846
847
848class DebianRepository:
849 """An abstraction to represent a repository."""
850
851 def __init__(
852 self,
853 enabled: bool,
854 repotype: str,
855 uri: str,
856 release: str,
857 groups: List[str],
858 filename: Optional[str] = "",
859 gpg_key_filename: Optional[str] = "",
860 options: Optional[dict] = None,
861 ):
862 self._enabled = enabled
863 self._repotype = repotype
864 self._uri = uri
865 self._release = release
866 self._groups = groups
867 self._filename = filename
868 self._gpg_key_filename = gpg_key_filename
869 self._options = options
870
871 @property
872 def enabled(self):
873 """Return whether or not the repository is enabled."""
874 return self._enabled
875
876 @property
877 def repotype(self):
878 """Return whether it is binary or source."""
879 return self._repotype
880
881 @property
882 def uri(self):
883 """Return the URI."""
884 return self._uri
885
886 @property
887 def release(self):
888 """Return which Debian/Ubuntu releases it is valid for."""
889 return self._release
890
891 @property
892 def groups(self):
893 """Return the enabled package groups."""
894 return self._groups
895
896 @property
897 def filename(self):
898 """Returns the filename for a repository."""
899 return self._filename
900
901 @filename.setter
902 def filename(self, fname: str) -> None:
903 """Sets the filename used when a repo is written back to diskself.
904
905 Args:
906 fname: a filename to write the repository information to.
907 """
908 if not fname.endswith(".list"):
909 raise InvalidSourceError("apt source filenames should end in .list!")
910
911 self._filename = fname
912
913 @property
914 def gpg_key(self):
915 """Returns the path to the GPG key for this repository."""
916 return self._gpg_key_filename
917
918 @property
919 def options(self):
920 """Returns any additional repo options which are set."""
921 return self._options
922
923 def make_options_string(self) -> str:
924 """Generate the complete options string for a a repository.
925
926 Combining `gpg_key`, if set, and the rest of the options to find
927 a complex repo string.
928 """
929 options = self._options if self._options else {}
930 if self._gpg_key_filename:
931 options["signed-by"] = self._gpg_key_filename
932
933 return (
934 "[{}] ".format(" ".join(["{}={}".format(k, v) for k, v in options.items()]))
935 if options
936 else ""
937 )
938
939 @staticmethod
940 def prefix_from_uri(uri: str) -> str:
941 """Get a repo list prefix from the uri, depending on whether a path is set."""
942 uridetails = urlparse(uri)
943 path = (
944 uridetails.path.lstrip("/").replace("/", "-") if uridetails.path else uridetails.netloc
945 )
946 return "/etc/apt/sources.list.d/{}".format(path)
947
948 @staticmethod
949 def from_repo_line(repo_line: str, write_file: Optional[bool] = True) -> "DebianRepository":
950 """Instantiate a new `DebianRepository` a `sources.list` entry line.
951
952 Args:
953 repo_line: a string representing a repository entry
954 write_file: boolean to enable writing the new repo to disk
955 """
956 repo = RepositoryMapping._parse(repo_line, "UserInput")
957 fname = "{}-{}.list".format(
958 DebianRepository.prefix_from_uri(repo.uri), repo.release.replace("/", "-")
959 )
960 repo.filename = fname
961
962 options = repo.options if repo.options else {}
963 if repo.gpg_key:
964 options["signed-by"] = repo.gpg_key
965
966 # For Python 3.5 it's required to use sorted in the options dict in order to not have
967 # different results in the order of the options between executions.
968 options_str = (
969 "[{}] ".format(" ".join(["{}={}".format(k, v) for k, v in sorted(options.items())]))
970 if options
971 else ""
972 )
973
974 if write_file:
975 with open(fname, "wb") as f:
976 f.write(
977 (
978 "{}".format("#" if not repo.enabled else "")
979 + "{} {}{} ".format(repo.repotype, options_str, repo.uri)
980 + "{} {}\n".format(repo.release, " ".join(repo.groups))
981 ).encode("utf-8")
982 )
983
984 return repo
985
986 def disable(self) -> None:
987 """Remove this repository from consideration.
988
989 Disable it instead of removing from the repository file.
990 """
991 searcher = "{} {}{} {}".format(
992 self.repotype, self.make_options_string(), self.uri, self.release
993 )
994 for line in fileinput.input(self._filename, inplace=True):
995 if re.match(r"^{}\s".format(re.escape(searcher)), line):
996 print("# {}".format(line), end="")
997 else:
998 print(line, end="")
999
1000 def import_key(self, key: str) -> None:
1001 """Import an ASCII Armor key.
1002
1003 A Radix64 format keyid is also supported for backwards
1004 compatibility. In this case Ubuntu keyserver will be
1005 queried for a key via HTTPS by its keyid. This method
1006 is less preferrable because https proxy servers may
1007 require traffic decryption which is equivalent to a
1008 man-in-the-middle attack (a proxy server impersonates
1009 keyserver TLS certificates and has to be explicitly
1010 trusted by the system).
1011
1012 Args:
1013 key: A GPG key in ASCII armor format,
1014 including BEGIN and END markers or a keyid.
1015
1016 Raises:
1017 GPGKeyError if the key could not be imported
1018 """
1019 key = key.strip()
1020 if "-" in key or "\n" in key:
1021 # Send everything not obviously a keyid to GPG to import, as
1022 # we trust its validation better than our own. eg. handling
1023 # comments before the key.
1024 logger.debug("PGP key found (looks like ASCII Armor format)")
1025 if (
1026 "-----BEGIN PGP PUBLIC KEY BLOCK-----" in key
1027 and "-----END PGP PUBLIC KEY BLOCK-----" in key
1028 ):
1029 logger.debug("Writing provided PGP key in the binary format")
1030 key_bytes = key.encode("utf-8")
1031 key_name = self._get_keyid_by_gpg_key(key_bytes)
1032 key_gpg = self._dearmor_gpg_key(key_bytes)
1033 self._gpg_key_filename = "/etc/apt/trusted.gpg.d/{}.gpg".format(key_name)
1034 self._write_apt_gpg_keyfile(key_name=self._gpg_key_filename, key_material=key_gpg)
1035 else:
1036 raise GPGKeyError("ASCII armor markers missing from GPG key")
1037 else:
1038 logger.warning(
1039 "PGP key found (looks like Radix64 format). "
1040 "SECURELY importing PGP key from keyserver; "
1041 "full key not provided."
1042 )
1043 # as of bionic add-apt-repository uses curl with an HTTPS keyserver URL
1044 # to retrieve GPG keys. `apt-key adv` command is deprecated as is
1045 # apt-key in general as noted in its manpage. See lp:1433761 for more
1046 # history. Instead, /etc/apt/trusted.gpg.d is used directly to drop
1047 # gpg
1048 key_asc = self._get_key_by_keyid(key)
1049 # write the key in GPG format so that apt-key list shows it
1050 key_gpg = self._dearmor_gpg_key(key_asc.encode("utf-8"))
1051 self._gpg_key_filename = "/etc/apt/trusted.gpg.d/{}.gpg".format(key)
1052 self._write_apt_gpg_keyfile(key_name=key, key_material=key_gpg)
1053
1054 @staticmethod
1055 def _get_keyid_by_gpg_key(key_material: bytes) -> str:
1056 """Get a GPG key fingerprint by GPG key material.
1057
1058 Gets a GPG key fingerprint (40-digit, 160-bit) by the ASCII armor-encoded
1059 or binary GPG key material. Can be used, for example, to generate file
1060 names for keys passed via charm options.
1061 """
1062 # Use the same gpg command for both Xenial and Bionic
1063 cmd = ["gpg", "--with-colons", "--with-fingerprint"]
1064 ps = subprocess.run(
1065 cmd,
1066 stdout=PIPE,
1067 stderr=PIPE,
1068 input=key_material,
1069 )
1070 out, err = ps.stdout.decode(), ps.stderr.decode()
1071 if "gpg: no valid OpenPGP data found." in err:
1072 raise GPGKeyError("Invalid GPG key material provided")
1073 # from gnupg2 docs: fpr :: Fingerprint (fingerprint is in field 10)
1074 return re.search(r"^fpr:{9}([0-9A-F]{40}):$", out, re.MULTILINE).group(1)
1075
1076 @staticmethod
1077 def _get_key_by_keyid(keyid: str) -> str:
1078 """Get a key via HTTPS from the Ubuntu keyserver.
1079
1080 Different key ID formats are supported by SKS keyservers (the longer ones
1081 are more secure, see "dead beef attack" and https://evil32.com/). Since
1082 HTTPS is used, if SSLBump-like HTTPS proxies are in place, they will
1083 impersonate keyserver.ubuntu.com and generate a certificate with
1084 keyserver.ubuntu.com in the CN field or in SubjAltName fields of a
1085 certificate. If such proxy behavior is expected it is necessary to add the
1086 CA certificate chain containing the intermediate CA of the SSLBump proxy to
1087 every machine that this code runs on via ca-certs cloud-init directive (via
1088 cloudinit-userdata model-config) or via other means (such as through a
1089 custom charm option). Also note that DNS resolution for the hostname in a
1090 URL is done at a proxy server - not at the client side.
1091 8-digit (32 bit) key ID
1092 https://keyserver.ubuntu.com/pks/lookup?search=0x4652B4E6
1093 16-digit (64 bit) key ID
1094 https://keyserver.ubuntu.com/pks/lookup?search=0x6E85A86E4652B4E6
1095 40-digit key ID:
1096 https://keyserver.ubuntu.com/pks/lookup?search=0x35F77D63B5CEC106C577ED856E85A86E4652B4E6
1097
1098 Args:
1099 keyid: An 8, 16 or 40 hex digit keyid to find a key for
1100
1101 Returns:
1102 A string contining key material for the specified GPG key id
1103
1104
1105 Raises:
1106 subprocess.CalledProcessError
1107 """
1108 # options=mr - machine-readable output (disables html wrappers)
1109 keyserver_url = (
1110 "https://keyserver.ubuntu.com" "/pks/lookup?op=get&options=mr&exact=on&search=0x{}"
1111 )
1112 curl_cmd = ["curl", keyserver_url.format(keyid)]
1113 # use proxy server settings in order to retrieve the key
1114 return check_output(curl_cmd).decode()
1115
1116 @staticmethod
1117 def _dearmor_gpg_key(key_asc: bytes) -> bytes:
1118 """Converts a GPG key in the ASCII armor format to the binary format.
1119
1120 Args:
1121 key_asc: A GPG key in ASCII armor format.
1122
1123 Returns:
1124 A GPG key in binary format as a string
1125
1126 Raises:
1127 GPGKeyError
1128 """
1129 ps = subprocess.run(["gpg", "--dearmor"], stdout=PIPE, stderr=PIPE, input=key_asc)
1130 out, err = ps.stdout, ps.stderr.decode()
1131 if "gpg: no valid OpenPGP data found." in err:
1132 raise GPGKeyError(
1133 "Invalid GPG key material. Check your network setup"
1134 " (MTU, routing, DNS) and/or proxy server settings"
1135 " as well as destination keyserver status."
1136 )
1137 else:
1138 return out
1139
1140 @staticmethod
1141 def _write_apt_gpg_keyfile(key_name: str, key_material: bytes) -> None:
1142 """Writes GPG key material into a file at a provided path.
1143
1144 Args:
1145 key_name: A key name to use for a key file (could be a fingerprint)
1146 key_material: A GPG key material (binary)
1147 """
1148 with open(key_name, "wb") as keyf:
1149 keyf.write(key_material)
1150
1151
1152class RepositoryMapping(Mapping):
1153 """An representation of known repositories.
1154
1155 Instantiation of `RepositoryMapping` will iterate through the
1156 filesystem, parse out repository files in `/etc/apt/...`, and create
1157 `DebianRepository` objects in this list.
1158
1159 Typical usage:
1160
1161 repositories = apt.RepositoryMapping()
1162 repositories.add(DebianRepository(
1163 enabled=True, repotype="deb", uri="https://example.com", release="focal",
1164 groups=["universe"]
1165 ))
1166 """
1167
1168 def __init__(self):
1169 self._repository_map = {}
1170 # Repositories that we're adding -- used to implement mode param
1171 self.default_file = "/etc/apt/sources.list"
1172
1173 # read sources.list if it exists
1174 if os.path.isfile(self.default_file):
1175 self.load(self.default_file)
1176
1177 # read sources.list.d
1178 for file in glob.iglob("/etc/apt/sources.list.d/*.list"):
1179 self.load(file)
1180
1181 def __contains__(self, key: str) -> bool:
1182 """Magic method for checking presence of repo in mapping."""
1183 return key in self._repository_map
1184
1185 def __len__(self) -> int:
1186 """Return number of repositories in map."""
1187 return len(self._repository_map)
1188
1189 def __iter__(self) -> Iterable[DebianRepository]:
1190 """Iterator magic method for RepositoryMapping."""
1191 return iter(self._repository_map.values())
1192
1193 def __getitem__(self, repository_uri: str) -> DebianRepository:
1194 """Return a given `DebianRepository`."""
1195 return self._repository_map[repository_uri]
1196
1197 def __setitem__(self, repository_uri: str, repository: DebianRepository) -> None:
1198 """Add a `DebianRepository` to the cache."""
1199 self._repository_map[repository_uri] = repository
1200
1201 def load(self, filename: str):
1202 """Load a repository source file into the cache.
1203
1204 Args:
1205 filename: the path to the repository file
1206 """
1207 parsed = []
1208 skipped = []
1209 with open(filename, "r") as f:
1210 for n, line in enumerate(f):
1211 try:
1212 repo = self._parse(line, filename)
1213 except InvalidSourceError:
1214 skipped.append(n)
1215 else:
1216 repo_identifier = "{}-{}-{}".format(repo.repotype, repo.uri, repo.release)
1217 self._repository_map[repo_identifier] = repo
1218 parsed.append(n)
1219 logger.debug("parsed repo: '%s'", repo_identifier)
1220
1221 if skipped:
1222 skip_list = ", ".join(str(s) for s in skipped)
1223 logger.debug("skipped the following lines in file '%s': %s", filename, skip_list)
1224
1225 if parsed:
1226 logger.info("parsed %d apt package repositories", len(parsed))
1227 else:
1228 raise InvalidSourceError("all repository lines in '{}' were invalid!".format(filename))
1229
1230 @staticmethod
1231 def _parse(line: str, filename: str) -> DebianRepository:
1232 """Parse a line in a sources.list file.
1233
1234 Args:
1235 line: a single line from `load` to parse
1236 filename: the filename being read
1237
1238 Raises:
1239 InvalidSourceError if the source type is unknown
1240 """
1241 enabled = True
1242 repotype = uri = release = gpg_key = ""
1243 options = {}
1244 groups = []
1245
1246 line = line.strip()
1247 if line.startswith("#"):
1248 enabled = False
1249 line = line[1:]
1250
1251 # Check for "#" in the line and treat a part after it as a comment then strip it off.
1252 i = line.find("#")
1253 if i > 0:
1254 line = line[:i]
1255
1256 # Split a source into substrings to initialize a new repo.
1257 source = line.strip()
1258 if source:
1259 # Match any repo options, and get a dict representation.
1260 for v in re.findall(OPTIONS_MATCHER, source):
1261 opts = dict(o.split("=") for o in v.strip("[]").split())
1262 # Extract the 'signed-by' option for the gpg_key
1263 gpg_key = opts.pop("signed-by", "")
1264 options = opts
1265
1266 # Remove any options from the source string and split the string into chunks
1267 source = re.sub(OPTIONS_MATCHER, "", source)
1268 chunks = source.split()
1269
1270 # Check we've got a valid list of chunks
1271 if len(chunks) < 3 or chunks[0] not in VALID_SOURCE_TYPES:
1272 raise InvalidSourceError("An invalid sources line was found in %s!", filename)
1273
1274 repotype = chunks[0]
1275 uri = chunks[1]
1276 release = chunks[2]
1277 groups = chunks[3:]
1278
1279 return DebianRepository(
1280 enabled, repotype, uri, release, groups, filename, gpg_key, options
1281 )
1282 else:
1283 raise InvalidSourceError("An invalid sources line was found in %s!", filename)
1284
1285 def add(self, repo: DebianRepository, default_filename: Optional[bool] = False) -> None:
1286 """Add a new repository to the system.
1287
1288 Args:
1289 repo: a `DebianRepository` object
1290 default_filename: an (Optional) filename if the default is not desirable
1291 """
1292 new_filename = "{}-{}.list".format(
1293 DebianRepository.prefix_from_uri(repo.uri), repo.release.replace("/", "-")
1294 )
1295
1296 fname = repo.filename or new_filename
1297
1298 options = repo.options if repo.options else {}
1299 if repo.gpg_key:
1300 options["signed-by"] = repo.gpg_key
1301
1302 with open(fname, "wb") as f:
1303 f.write(
1304 (
1305 "{}".format("#" if not repo.enabled else "")
1306 + "{} {}{} ".format(repo.repotype, repo.make_options_string(), repo.uri)
1307 + "{} {}\n".format(repo.release, " ".join(repo.groups))
1308 ).encode("utf-8")
1309 )
1310
1311 self._repository_map["{}-{}-{}".format(repo.repotype, repo.uri, repo.release)] = repo
1312
1313 def disable(self, repo: DebianRepository) -> None:
1314 """Remove a repository. Disable by default.
1315
1316 Args:
1317 repo: a `DebianRepository` to disable
1318 """
1319 searcher = "{} {}{} {}".format(
1320 repo.repotype, repo.make_options_string(), repo.uri, repo.release
1321 )
1322
1323 for line in fileinput.input(repo.filename, inplace=True):
1324 if re.match(r"^{}\s".format(re.escape(searcher)), line):
1325 print("# {}".format(line), end="")
1326 else:
1327 print(line, end="")
1328
1329 self._repository_map["{}-{}-{}".format(repo.repotype, repo.uri, repo.release)] = repo
diff --git a/metadata.yaml b/metadata.yaml
index de1439f..5becf33 100644
--- a/metadata.yaml
+++ b/metadata.yaml
@@ -1,23 +1,14 @@
1# Copyright 2022 Barry Price1name: charm-tor-hidden-service
2# See LICENSE file for licensing details.2display-name: Tor Hidden Service Charmed Operator
33summary: Tor Hidden Service Charmed Operator
4# For a complete list of supported options, see:
5# https://juju.is/docs/sdk/metadata-reference
6name: charm-tor
7display-name: |
8 TEMPLATE-TODO: fill out a display name for the Charmcraft store
9description: |4description: |
10 TEMPLATE-TODO: fill out the charm's description5 Tor is free software and an open network that helps you defend against
11summary: |6 traffic analysis, a form of network surveillance that threatens personal
12 TEMPLATE-TODO: fill out the charm's summary7 freedom and privacy, confidential business activities and relationships, and
138 state security.
14# TEMPLATE-TODO: replace with containers for your workload (delete for non-k8s)9requires:
15containers:10 reverseproxy:
16 httpbin:11 interface: http
17 resource: httpbin-image12series:
13 - jammy
1814
19# TEMPLATE-TODO: each container defined above must specify an oci-image resource
20resources:
21 httpbin-image:
22 type: oci-image
23 description: OCI image for httpbin (kennethreitz/httpbin)
diff --git a/pyproject.toml b/pyproject.toml
24new file mode 10064415new file mode 100644
index 0000000..177269a
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,38 @@
1# Copyright 2022 Canonical Ltd.
2# See LICENSE file for licensing details.
3
4# Testing tools configuration
5[tool.coverage.run]
6branch = true
7
8[tool.coverage.report]
9show_missing = true
10
11[tool.pytest.ini_options]
12minversion = "6.0"
13log_cli_level = "INFO"
14
15# Formatting tools configuration
16[tool.black]
17line-length = 99
18target-version = ["py310"]
19
20[tool.isort]
21profile = "black"
22
23# Linting tools configuration
24[tool.flake8]
25max-line-length = 99
26max-doc-length = 99
27max-complexity = 10
28exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
29select = ["E", "W", "F", "C", "N", "R", "D", "H"]
30# Ignore D107 Missing docstring in __init__
31ignore = ["D107"]
32# D100, D101, D102, D103: Ignore missing docstrings in tests
33per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"]
34docstring-convention = "google"
35# Check for properly formatted copyright header in each file
36copyright-check = "True"
37copyright-author = "Canonical Ltd."
38copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"
diff --git a/src/charm.py b/src/charm.py
index 2cac651..73b10fa 100755
--- a/src/charm.py
+++ b/src/charm.py
@@ -1,104 +1,206 @@
1#!/usr/bin/env python31#!/usr/bin/env python3
2# Copyright 2022 Barry Price2# Copyright 2022 Canonical Ltd.
3# See LICENSE file for licensing details.3# See LICENSE file for licensing details.
4#
5# Learn more at: https://juju.is/docs/sdk
64
7"""Charm the service.5"""Charmed Operator to provide Tor hidden services."""
8
9Refer to the following post for a quick-start guide that will help you
10develop a new k8s charm using the Operator Framework:
11
12 https://discourse.charmhub.io/t/4208
13"""
146
15import logging7import logging
168import os
17from ops.charm import CharmBase9import shutil
10import subprocess
11import urllib.request
12
13import jinja2
14
15from charms.operator_libs_linux.v0 import apt
16from ops.charm import (
17 CharmBase,
18 RelationChangedEvent,
19)
18from ops.framework import StoredState20from ops.framework import StoredState
19from ops.main import main21from ops.main import main
20from ops.model import ActiveStatus22from ops.model import (
23 BlockedStatus,
24 Unit,
25)
2126
22logger = logging.getLogger(__name__)27logger = logging.getLogger(__name__)
2328
2429
25class CharmTorCharm(CharmBase):30def get_series():
26 """Charm the service."""31 """Return the installed Ubuntu series (e.g. "jammy")."""
32 return subprocess.check_output(["lsb_release", "-sc"]).decode('utf-8').strip()
2733
28 _stored = StoredState()
2934
30 def __init__(self, *args):35def install_tor_repo():
31 super().__init__(*args)36 """Authenticate, install and pin the tor package to the torproject.org repo."""
32 self.framework.observe(self.on.httpbin_pebble_ready, self._on_httpbin_pebble_ready)37 # Installation is based on Tor installation instructions for Debian/Ubuntu:
33 self.framework.observe(self.on.config_changed, self._on_config_changed)38 # https://support.torproject.org/apt/tor-deb-repo/
34 self.framework.observe(self.on.fortune_action, self._on_fortune_action)39 keyring_path = "/usr/share/keyrings/tor-archive-keyring.gpg"
35 self._stored.set_default(things=[])40 base_url = "https://deb.torproject.org/torproject.org"
36
37 def _on_httpbin_pebble_ready(self, event):
38 """Define and start a workload using the Pebble API.
39
40 TEMPLATE-TODO: change this example to suit your needs.
41 You'll need to specify the right entrypoint and environment
42 configuration for your specific workload. Tip: you can see the
43 standard entrypoint of an existing container using docker inspect
44
45 Learn more about Pebble layers at https://github.com/canonical/pebble
46 """
47 # Get a reference the container attribute on the PebbleReadyEvent
48 container = event.workload
49 # Define an initial Pebble layer configuration
50 pebble_layer = {
51 "summary": "httpbin layer",
52 "description": "pebble config layer for httpbin",
53 "services": {
54 "httpbin": {
55 "override": "replace",
56 "summary": "httpbin",
57 "command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent",
58 "startup": "enabled",
59 "environment": {"thing": self.model.config["thing"]},
60 }
61 },
62 }
63 # Add initial Pebble config layer using the Pebble API
64 container.add_layer("httpbin", pebble_layer, combine=True)
65 # Autostart any services that were defined with startup: enabled
66 container.autostart()
67 # Learn more about statuses in the SDK docs:
68 # https://juju.is/docs/sdk/constructs#heading--statuses
69 self.unit.status = ActiveStatus()
7041
71 def _on_config_changed(self, _):42 # retrieve the signing key
72 """Just an example to show how to deal with changed configuration.43 keyring_url = "{}/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc".format(base_url)
44 with urllib.request.urlopen(keyring_url) as f:
45 keyring = f.read()
46
47 with open(keyring_path, "wb") as f:
48 f.write(keyring)
49
50 # identify our running series
51 series = get_series()
52
53 # build .list file
54 tor_list = "deb [signed-by={}] {} {} main\n".format(keyring_path, base_url, series)
55 tor_list += "deb-src [signed-by={}] {} {} main\n".format(keyring_path, base_url, series)
7356
74 TEMPLATE-TODO: change this example to suit your needs.57 # write .list file
75 If you don't need to handle config, you can remove this method,58 with open("/etc/apt/sources.list.d/tor.list", "wb") as f:
76 the hook created in __init__.py for it, the corresponding test,59 f.write(tor_list.encode('utf-8'))
77 and the config.py file.
7860
79 Learn more about config at https://juju.is/docs/sdk/config61 # pin the tor package to this repo
80 """62 pin_config = "Package: tor\n"
81 current = self.config["thing"]63 pin_config += "Pin: release c=main\n"
82 if current not in self._stored.things:64 pin_config += "Pin-Priority: 900\n\n"
83 logger.debug("found a new thing: %r", current)
84 self._stored.things.append(current)
8565
86 def _on_fortune_action(self, event):66 # don't use the universe version
87 """Just an example to show how to receive actions.67 pin_config += "Package: tor\n"
68 pin_config += "Pin: release c=universe\n"
69 pin_config += "Pin-Priority: 1\n\n"
8870
89 TEMPLATE-TODO: change this example to suit your needs.71 with open("/etc/apt/preferences.d/tor-pin", "wb") as f:
90 If you don't need to handle actions, you can remove this method,72 f.write(pin_config.encode('utf-8'))
91 the hook created in __init__.py for it, the corresponding test,
92 and the actions.py file.
9373
94 Learn more about actions at https://juju.is/docs/sdk/actions74
95 """75def remove_tor_repo():
96 fail = event.params["fail"]76 """Remove and unpin the torproject.org repo."""
97 if fail:77 paths = (
98 event.fail(fail)78 "/etc/apt/sources.list.d/tor.list",
99 else:79 "/etc/apt/preferences.d/tor-pin",
100 event.set_results({"fortune": "A bug in the code is worth two in the documentation."})80 "/usr/share/keyrings/tor-archive-keyring.gpg",
81 )
82 for p in paths:
83 if os.path.isfile(p):
84 os.unlink(p)
85
86
87class TorCharm(CharmBase):
88 """Charm the tor service."""
89
90 _state = StoredState()
91
92 def __init__(self, *args):
93 super().__init__(*args)
94 self._state.set_default(
95 need_package_apt_transport_https=True,
96 need_package_gpg=True,
97 need_package_tor=True,
98 )
99 self.framework.observe(self.on.config_changed, self._on_config_changed)
100 self.framework.observe(
101 self.on.reverseproxy_relation_changed, self._on_reverseproxy_relation_changed
102 )
103
104 def _on_config_changed(self, _):
105 """Handle config changes."""
106 # ensure required packages are installed
107 self._ensure_apt_transport_https()
108 self._ensure_gpg()
109 self._ensure_tor()
110
111 def _ensure_apt_transport_https(self):
112 if self._state.need_package_apt_transport_https:
113 logger.info("Ensuring apt-transport-https is installed")
114 apt.add_package("apt-transport-https", update_cache=True)
115 self._state.need_package_apt_transport_https = False
116
117 def _ensure_gpg(self):
118 if self._state.need_package_gpg:
119 logger.info("Ensuring gpg is installed")
120 apt.add_package("gpg", update_cache=True)
121 self._state.need_package_gpg = False
122
123 def _ensure_tor(self):
124 if self._state.need_package_tor:
125 tor_source = self.model.config['tor_source']
126 if tor_source == 'ubuntu':
127 logger.info("Source is ubuntu, ensuring torproject repo is not set up")
128 remove_tor_repo()
129 elif tor_source == 'torproject':
130 logger.info("Source is torproject, ensuring torproject repo is set up")
131 install_tor_repo()
132 else:
133 err = "Unexpected tor_source value: {}".format(tor_source)
134 logger.error(err)
135 self.unit.status = BlockedStatus(err)
136 logger.info("Ensuring tor is installed")
137 apt.add_package("tor", update_cache=True)
138
139 def _tor_setup(self, services):
140 cfg = {}
141 cfg['socks5_port'] = self.model.config['socks5_port']
142 root_dir = '/var/lib/tor'
143 for k, v in services.items():
144 logger.info('Got service name {} with value {}'.format(k, v))
145 service_dir = '{}/{}'.format(root_dir, v.get('servername'))
146 if not os.path.isdir(service_dir):
147 subprocess.check_call(
148 ['install', '-d', service_dir, '-o', 'debian-tor', '-m', '700']
149 )
150
151 for file in os.listdir(root_dir):
152 if os.path.isdir(file):
153 basename = os.path.basename(file)
154 for k, v in services.items():
155 if k.get('servername') == basename:
156 # keep this configured directory
157 pass
158 else:
159 logger.info('Removing unconfigured site {}'.format(basename))
160 shutil.rmtree(file)
161
162 # NOTE: Once the below closed issue is actually resolved[1], this function
163 # should be replaced with a built-in one in Operator Framework.
164 # [1]: https://github.com/canonical/operator/issues/228
165
166 list_services = []
167 for k, v in services.items():
168 list_services.append(v)
169
170 templates = jinja2.Environment(
171 loader=jinja2.FileSystemLoader(self.charm_dir / "templates"),
172 )
173 template = templates.get_template("torrc")
174 torrc = template.render({'cfg': cfg, 'services': list_services})
175
176 torrc_filename = '/etc/tor/torrc'
177
178 with open(torrc_filename, 'wb') as f:
179 f.write(torrc.encode('utf-8'))
180
181 def _on_reverseproxy_relation_changed(self, event: RelationChangedEvent):
182 unit_data = event.relation.data
183 logger.info(unit_data)
184 services = {}
185 for unit in unit_data:
186 if not isinstance(unit, Unit):
187 logger.info("Skipping data of type {}".format(type(unit)))
188 continue
189 if unit.name == self.unit.name:
190 logger.info("Skipping our own unit ({})".format(unit.name))
191 continue
192 logger.info("Found related unit {}".format(unit))
193
194 services.update({
195 unit.name: {
196 'hostname': unit_data[unit].get('hostname'),
197 'private_address': unit_data[unit].get('private-address'),
198 'port': unit_data[unit].get('port'),
199 'servername': unit_data[unit].get('servername'),
200 }
201 })
202 self._tor_setup(services)
101203
102204
103if __name__ == "__main__":205if __name__ == "__main__":
104 main(CharmTorCharm)206 main(TorCharm)
diff --git a/templates/torrc b/templates/torrc
105new file mode 100644207new file mode 100644
index 0000000..d709005
--- /dev/null
+++ b/templates/torrc
@@ -0,0 +1,12 @@
1# torrc generated by Juju.
2# Do not edit, changes are subject to being overwritten.
3
4SocksPort 127.0.0.1:{{ cfg.socks5_port }}
5
6{% for service in services %}
7HiddenServiceDir /var/lib/tor/{{ service.servername }}/
8# service.hostname is {{ service.hostname }}
9# service.private-address is {{ service.private_address }}
10HiddenServicePort {{ service.port }} {{ service.private_address }}:{{ service.port }}
11
12{% endfor %}
diff --git a/tests/__init__.py b/tests/__init__.py
index e163492..0c77dd5 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,2 +1,6 @@
1# Copyright 2022 Canonical Ltd.
2# See LICENSE file for licensing details.
3
1import ops.testing4import ops.testing
5
2ops.testing.SIMULATE_CAN_CONNECT = True6ops.testing.SIMULATE_CAN_CONNECT = True
diff --git a/tests/test_charm.py b/tests/test_charm.py
index 0830b8d..c7e4d40 100644
--- a/tests/test_charm.py
+++ b/tests/test_charm.py
@@ -1,15 +1,12 @@
1# Copyright 2022 Barry Price1# Copyright 2022 Canonical Ltd.
2# See LICENSE file for licensing details.2# See LICENSE file for licensing details.
3#
4# Learn more about testing at: https://juju.is/docs/sdk/testing
53
6import unittest4import unittest
7from unittest.mock import Mock
85
9from charm import CharmTorCharm
10from ops.model import ActiveStatus
11from ops.testing import Harness6from ops.testing import Harness
127
8from charm import CharmTorCharm
9
1310
14class TestCharm(unittest.TestCase):11class TestCharm(unittest.TestCase):
15 def setUp(self):12 def setUp(self):
@@ -19,50 +16,5 @@ class TestCharm(unittest.TestCase):
1916
20 def test_config_changed(self):17 def test_config_changed(self):
21 self.assertEqual(list(self.harness.charm._stored.things), [])18 self.assertEqual(list(self.harness.charm._stored.things), [])
22 self.harness.update_config({"thing": "foo"})19 self.harness.update_config({"tor-mode": "hidden"})
23 self.assertEqual(list(self.harness.charm._stored.things), ["foo"])20 self.assertEqual(list(self.harness.charm._stored.things), ["hidden"])
24
25 def test_action(self):
26 # the harness doesn't (yet!) help much with actions themselves
27 action_event = Mock(params={"fail": ""})
28 self.harness.charm._on_fortune_action(action_event)
29
30 self.assertTrue(action_event.set_results.called)
31
32 def test_action_fail(self):
33 action_event = Mock(params={"fail": "fail this"})
34 self.harness.charm._on_fortune_action(action_event)
35
36 self.assertEqual(action_event.fail.call_args, [("fail this",)])
37
38 def test_httpbin_pebble_ready(self):
39 # Simulate making the Pebble socket available
40 self.harness.set_can_connect("httpbin", True)
41 # Check the initial Pebble plan is empty
42 initial_plan = self.harness.get_container_pebble_plan("httpbin")
43 self.assertEqual(initial_plan.to_yaml(), "{}\n")
44 # Expected plan after Pebble ready with default config
45 expected_plan = {
46 "services": {
47 "httpbin": {
48 "override": "replace",
49 "summary": "httpbin",
50 "command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent",
51 "startup": "enabled",
52 "environment": {"thing": "🎁"},
53 }
54 },
55 }
56 # Get the httpbin container from the model
57 container = self.harness.model.unit.get_container("httpbin")
58 # Emit the PebbleReadyEvent carrying the httpbin container
59 self.harness.charm.on.httpbin_pebble_ready.emit(container)
60 # Get the plan now we've run PebbleReady
61 updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict()
62 # Check we've got the plan we expected
63 self.assertEqual(expected_plan, updated_plan)
64 # Check the service was started
65 service = self.harness.model.unit.get_container("httpbin").get_service("httpbin")
66 self.assertTrue(service.is_running())
67 # Ensure we set an ActiveStatus with no message
68 self.assertEqual(self.harness.model.unit.status, ActiveStatus())
diff --git a/tox.ini b/tox.ini
69new file mode 10064421new file mode 100644
index 0000000..bca0088
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,78 @@
1# Copyright 2022 Canonical Ltd.
2# See LICENSE file for licensing details.
3
4[tox]
5skipsdist=True
6skip_missing_interpreters = True
7envlist = lint, unit
8
9[vars]
10src_path = {toxinidir}/src/
11tst_path = {toxinidir}/tests/
12lib_path = {toxinidir}/lib/charms/operator_libs_linux
13all_path = {[vars]src_path} {[vars]tst_path}
14
15[testenv]
16setenv =
17 PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path}
18 PYTHONBREAKPOINT=ipdb.set_trace
19 PY_COLORS=1
20passenv =
21 PYTHONPATH
22 HOME
23 PATH
24 CHARM_BUILD_DIR
25 MODEL_SETTINGS
26 HTTP_PROXY
27 HTTPS_PROXY
28 NO_PROXY
29
30[testenv:fmt]
31description = Apply coding style standards to code
32deps =
33 black
34 isort
35commands =
36 isort {[vars]all_path}
37 black {[vars]all_path}
38
39[testenv:lint]
40description = Check code against coding style standards
41deps =
42 black
43 flake8
44 flake8-docstrings
45 flake8-copyright
46 flake8-builtins
47 pyproject-flake8
48 pep8-naming
49 isort
50 codespell
51commands =
52 codespell {toxinidir}/*.yaml {toxinidir}/*.ini {toxinidir}/*.md \
53 {toxinidir}/*.toml {toxinidir}/*.txt {toxinidir}/.github
54 # pflake8 wrapper supports config from pyproject.toml
55 pflake8 {[vars]all_path}
56 isort --check-only --diff {[vars]all_path}
57 black --check --diff {[vars]all_path}
58
59[testenv:unit]
60description = Run unit tests
61deps =
62 pytest
63 coverage[toml]
64 -r{toxinidir}/requirements.txt
65commands =
66 coverage run --source={[vars]src_path} \
67 -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs}
68 coverage report
69
70[testenv:integration]
71description = Run integration tests
72deps =
73 git+https://github.com/juju/python-libjuju.git
74 pytest
75 git+https://github.com/charmed-kubernetes/pytest-operator.git
76 -r{toxinidir}/requirements.txt
77commands =
78 pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs}

Subscribers

People subscribed via source and target branches

to all changes: