genesis-devtools 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- genesis_devtools/__init__.py +0 -0
- genesis_devtools/builder/__init__.py +17 -0
- genesis_devtools/builder/base.py +196 -0
- genesis_devtools/builder/builder.py +108 -0
- genesis_devtools/builder/dependency.py +125 -0
- genesis_devtools/builder/packer.py +201 -0
- genesis_devtools/cmd/__init__.py +0 -0
- genesis_devtools/cmd/cli.py +390 -0
- genesis_devtools/constants.py +33 -0
- genesis_devtools/libvirt.py +346 -0
- genesis_devtools/logger.py +78 -0
- genesis_devtools/packer/genesis_core/genesis-core.pkr.hcl +110 -0
- genesis_devtools/packer/genesis_core/plugins.pkr.hcl +12 -0
- genesis_devtools/packer/ubuntu_24/plugins.pkr.hcl +12 -0
- genesis_devtools/packer/ubuntu_24/ubuntu-24.pkr.hcl +118 -0
- genesis_devtools/tests/__init__.py +0 -0
- genesis_devtools/tests/unit/__init__.py +0 -0
- genesis_devtools/tests/unit/conftest.py +69 -0
- genesis_devtools/tests/unit/test_basic.py +20 -0
- genesis_devtools/tests/unit/test_builder.py +45 -0
- genesis_devtools/tests/unit/test_dependency.py +85 -0
- genesis_devtools/tests/unit/test_packer.py +34 -0
- genesis_devtools/utils.py +166 -0
- genesis_devtools-0.0.4.dist-info/AUTHORS +1 -0
- genesis_devtools-0.0.4.dist-info/LICENSE +201 -0
- genesis_devtools-0.0.4.dist-info/METADATA +264 -0
- genesis_devtools-0.0.4.dist-info/RECORD +31 -0
- genesis_devtools-0.0.4.dist-info/WHEEL +5 -0
- genesis_devtools-0.0.4.dist-info/entry_points.txt +2 -0
- genesis_devtools-0.0.4.dist-info/pbr.json +1 -0
- genesis_devtools-0.0.4.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright 2025 Genesis Corporation.
|
|
2
|
+
#
|
|
3
|
+
# All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
6
|
+
# not use this file except in compliance with the License. You may obtain
|
|
7
|
+
# a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
13
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
14
|
+
# License for the specific language governing permissions and limitations
|
|
15
|
+
# under the License.
|
|
16
|
+
|
|
17
|
+
from genesis_devtools.builder import dependency
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Copyright 2025 Genesis Corporation.
|
|
2
|
+
#
|
|
3
|
+
# All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
6
|
+
# not use this file except in compliance with the License. You may obtain
|
|
7
|
+
# a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
13
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
14
|
+
# License for the specific language governing permissions and limitations
|
|
15
|
+
# under the License.
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import abc
|
|
19
|
+
import os
|
|
20
|
+
import typing as tp
|
|
21
|
+
|
|
22
|
+
from genesis_devtools import constants as c
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Image(tp.NamedTuple):
|
|
26
|
+
"""Image representation."""
|
|
27
|
+
|
|
28
|
+
script: str
|
|
29
|
+
profile: c.ImageProfileType = "ubuntu_24"
|
|
30
|
+
format: c.ImageFormatType = "raw"
|
|
31
|
+
name: str | None = None
|
|
32
|
+
envs: tp.List[str] | None = None
|
|
33
|
+
override: tp.Dict[str, tp.Any] | None = None
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_config(
|
|
37
|
+
cls, image_config: tp.Dict[str, tp.Any], work_dir: str
|
|
38
|
+
) -> "Image":
|
|
39
|
+
"""Create an image from configuration."""
|
|
40
|
+
script = image_config.pop("script")
|
|
41
|
+
if not os.path.isabs(script):
|
|
42
|
+
script = os.path.join(work_dir, script)
|
|
43
|
+
return cls(script=script, **image_config)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Element(tp.NamedTuple):
|
|
47
|
+
"""Element representation."""
|
|
48
|
+
|
|
49
|
+
manifest: tp.Optional[str] = None
|
|
50
|
+
images: tp.Optional[tp.List[Image]] = None
|
|
51
|
+
artifacts: tp.Optional[tp.List[str]] = None
|
|
52
|
+
|
|
53
|
+
def __str__(self):
|
|
54
|
+
if self.manifest:
|
|
55
|
+
# TODO: Add implementation where manifest is used.
|
|
56
|
+
return "<Element manifest=...>"
|
|
57
|
+
|
|
58
|
+
if self.images and len(self.images) > 0:
|
|
59
|
+
name = ", ".join([f"{i.profile}" for i in self.images])
|
|
60
|
+
return f"<Element images={name}>"
|
|
61
|
+
|
|
62
|
+
return f"<Element {str(self)}>"
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_config(
|
|
66
|
+
cls, element_config: tp.Dict[str, tp.Any], work_dir: str
|
|
67
|
+
) -> "Element":
|
|
68
|
+
"""Create an element from configuration."""
|
|
69
|
+
image_configs = element_config.pop("images", [])
|
|
70
|
+
images = [Image.from_config(img, work_dir) for img in image_configs]
|
|
71
|
+
return cls(images=images, **element_config)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AbstractDependency(abc.ABC):
|
|
75
|
+
"""Abstract dependency item.
|
|
76
|
+
|
|
77
|
+
This class defines the interface for a dependency item.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
dependencies_store: tp.List["AbstractDependency"] = []
|
|
81
|
+
|
|
82
|
+
def __init_subclass__(cls, **kwargs) -> None:
|
|
83
|
+
super().__init_subclass__()
|
|
84
|
+
cls.dependencies_store.append(cls)
|
|
85
|
+
|
|
86
|
+
@abc.abstractproperty
|
|
87
|
+
def img_dest(self) -> tp.Optional[str]:
|
|
88
|
+
"""Destination for the image."""
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def local_path(self) -> tp.Optional[str]:
|
|
92
|
+
"""Local path to the dependency."""
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
@abc.abstractmethod
|
|
96
|
+
def fetch(self, output_dir: str) -> None:
|
|
97
|
+
"""Fetch the dependency."""
|
|
98
|
+
|
|
99
|
+
@abc.abstractclassmethod
|
|
100
|
+
def from_config(
|
|
101
|
+
cls, dep_config: tp.Dict[str, tp.Any], work_dir: str
|
|
102
|
+
) -> "AbstractDependency":
|
|
103
|
+
"""Create a dependency item from configuration."""
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def find_dependency(
|
|
107
|
+
cls, dep_config: tp.Dict[str, tp.Any], work_dir: str
|
|
108
|
+
) -> tp.Optional["AbstractDependency"]:
|
|
109
|
+
"""Probe all dependencies to find the right one."""
|
|
110
|
+
for dep in cls.dependencies_store:
|
|
111
|
+
try:
|
|
112
|
+
return dep.from_config(dep_config, work_dir)
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AbstractImageBuilder(abc.ABC):
|
|
120
|
+
"""Abstract image builder.
|
|
121
|
+
|
|
122
|
+
This class defines the interface for building images.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
@abc.abstractmethod
|
|
126
|
+
def pre_build(
|
|
127
|
+
self,
|
|
128
|
+
image_dir: str,
|
|
129
|
+
image: Image,
|
|
130
|
+
deps: tp.List[AbstractDependency],
|
|
131
|
+
developer_keys: tp.Optional[str] = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Actions to prepare the environment for building the image."""
|
|
134
|
+
|
|
135
|
+
@abc.abstractmethod
|
|
136
|
+
def build(
|
|
137
|
+
self,
|
|
138
|
+
image_dir: str,
|
|
139
|
+
image: Image,
|
|
140
|
+
developer_keys: tp.Optional[str] = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Actions to build the image."""
|
|
143
|
+
|
|
144
|
+
@abc.abstractmethod
|
|
145
|
+
def post_build(
|
|
146
|
+
self,
|
|
147
|
+
image_dir: str,
|
|
148
|
+
image: Image,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Actions to perform after building the image."""
|
|
151
|
+
|
|
152
|
+
def run(
|
|
153
|
+
self,
|
|
154
|
+
image_dir: str,
|
|
155
|
+
image: Image,
|
|
156
|
+
deps: tp.List[AbstractDependency],
|
|
157
|
+
developer_keys: tp.Optional[str] = None,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Run the image builder."""
|
|
160
|
+
self.pre_build(image_dir, image, deps, developer_keys)
|
|
161
|
+
self.build(image_dir, image, developer_keys)
|
|
162
|
+
self.post_build(image_dir, image)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class DummyImageBuilder(AbstractImageBuilder):
|
|
166
|
+
"""Dummy image builder.
|
|
167
|
+
|
|
168
|
+
Dummy builder that does nothing.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def pre_build(
|
|
172
|
+
self,
|
|
173
|
+
image_dir: str,
|
|
174
|
+
image: Image,
|
|
175
|
+
deps: tp.List[AbstractDependency],
|
|
176
|
+
developer_keys: tp.Optional[str] = None,
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Actions to prepare the environment for building the image."""
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def build(
|
|
182
|
+
self,
|
|
183
|
+
image_dir: str,
|
|
184
|
+
image: Image,
|
|
185
|
+
developer_keys: tp.Optional[str] = None,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Actions to build the image."""
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
def post_build(
|
|
191
|
+
self,
|
|
192
|
+
image_dir: str,
|
|
193
|
+
image: Image,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Actions to perform after building the image."""
|
|
196
|
+
return None
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Copyright 2025 Genesis Corporation.
|
|
2
|
+
#
|
|
3
|
+
# All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
6
|
+
# not use this file except in compliance with the License. You may obtain
|
|
7
|
+
# a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
13
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
14
|
+
# License for the specific language governing permissions and limitations
|
|
15
|
+
# under the License.
|
|
16
|
+
|
|
17
|
+
import typing as tp
|
|
18
|
+
import tempfile
|
|
19
|
+
|
|
20
|
+
from genesis_devtools.builder import base
|
|
21
|
+
from genesis_devtools.logger import AbstractLogger, DummyLogger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SimpleBuilder:
|
|
25
|
+
"""Abstract element builder.
|
|
26
|
+
|
|
27
|
+
This class defines the interface for building elements.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
DEP_KEY = "deps"
|
|
31
|
+
ELEMENT_KEY = "elements"
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
work_dir: str,
|
|
36
|
+
deps: tp.List[base.AbstractDependency],
|
|
37
|
+
elements: tp.List[base.Element],
|
|
38
|
+
image_builder: base.AbstractImageBuilder,
|
|
39
|
+
logger: tp.Optional[AbstractLogger] = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
super().__init__()
|
|
42
|
+
self._deps = deps
|
|
43
|
+
self._elements = elements
|
|
44
|
+
self._image_builder = image_builder
|
|
45
|
+
self._work_dir = work_dir
|
|
46
|
+
self._logger = logger or DummyLogger()
|
|
47
|
+
|
|
48
|
+
def fetch_dependency(self, deps_dir: str) -> None:
|
|
49
|
+
"""Fetch common dependencies for elements."""
|
|
50
|
+
self._logger.important("Fetching dependencies")
|
|
51
|
+
for dep in self._deps:
|
|
52
|
+
self._logger.info(f"Fetching dependency: {dep}")
|
|
53
|
+
dep.fetch(deps_dir)
|
|
54
|
+
|
|
55
|
+
def build(
|
|
56
|
+
self,
|
|
57
|
+
build_dir: tp.Optional[str] = None,
|
|
58
|
+
developer_keys: tp.Optional[str] = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Build all elements."""
|
|
61
|
+
self._logger.important("Building elements")
|
|
62
|
+
for e in self._elements:
|
|
63
|
+
self._logger.info(f"Building element: {e}")
|
|
64
|
+
for img in e.images:
|
|
65
|
+
# The build_dir is used only for debugging purposes to observe
|
|
66
|
+
# the content of the image. In production, the image is built
|
|
67
|
+
# in a temporary directory.
|
|
68
|
+
if build_dir is not None:
|
|
69
|
+
self._image_builder.run(
|
|
70
|
+
build_dir, img, self._deps, developer_keys
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
74
|
+
self._image_builder.run(
|
|
75
|
+
temp_dir, img, self._deps, developer_keys
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_config(
|
|
80
|
+
cls,
|
|
81
|
+
work_dir: str,
|
|
82
|
+
build_config: tp.Dict[str, tp.Any],
|
|
83
|
+
image_builder: base.AbstractImageBuilder,
|
|
84
|
+
logger: tp.Optional[AbstractLogger] = None,
|
|
85
|
+
) -> "SimpleBuilder":
|
|
86
|
+
"""Create a builder from configuration."""
|
|
87
|
+
# Prepare dependencies entries but do not fetch them
|
|
88
|
+
deps = []
|
|
89
|
+
dep_configs = build_config.get(cls.DEP_KEY, [])
|
|
90
|
+
for dep in dep_configs:
|
|
91
|
+
dep_item = base.AbstractDependency.find_dependency(dep, work_dir)
|
|
92
|
+
if dep_item is None:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Unable to handle dependency: {dep}. Unknown type."
|
|
95
|
+
)
|
|
96
|
+
deps.append(dep_item)
|
|
97
|
+
|
|
98
|
+
# Prepare elements
|
|
99
|
+
element_configs = build_config.get(cls.ELEMENT_KEY, [])
|
|
100
|
+
elements = [
|
|
101
|
+
base.Element.from_config(elem, work_dir)
|
|
102
|
+
for elem in element_configs
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
if not elements:
|
|
106
|
+
raise ValueError("No elements found in configuration")
|
|
107
|
+
|
|
108
|
+
return cls(work_dir, deps, elements, image_builder, logger)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Copyright 2025 Genesis Corporation.
|
|
2
|
+
#
|
|
3
|
+
# All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
6
|
+
# not use this file except in compliance with the License. You may obtain
|
|
7
|
+
# a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
13
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
14
|
+
# License for the specific language governing permissions and limitations
|
|
15
|
+
# under the License.
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import typing as tp
|
|
21
|
+
|
|
22
|
+
import bazooka
|
|
23
|
+
|
|
24
|
+
from genesis_devtools.builder import base
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LocalPathDependency(base.AbstractDependency):
|
|
28
|
+
"""Local path dependency item."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, path: str, img_dest: str) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
self._path = path
|
|
33
|
+
self._img_dest = img_dest
|
|
34
|
+
self._local_path = None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def img_dest(self) -> str | None:
|
|
38
|
+
"""Destination for the image."""
|
|
39
|
+
return self._img_dest
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def local_path(self) -> str | None:
|
|
43
|
+
"""Local path to the dependency."""
|
|
44
|
+
return self._local_path
|
|
45
|
+
|
|
46
|
+
def fetch(self, output_dir: str) -> None:
|
|
47
|
+
"""Fetch the dependency."""
|
|
48
|
+
path = self._path
|
|
49
|
+
if os.path.isdir(path):
|
|
50
|
+
name = os.path.basename(path)
|
|
51
|
+
shutil.copytree(path, os.path.join(output_dir, name))
|
|
52
|
+
self._local_path = os.path.join(output_dir, name)
|
|
53
|
+
else:
|
|
54
|
+
shutil.copy(path, output_dir)
|
|
55
|
+
self._local_path = os.path.join(output_dir, os.path.basename(path))
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
return f"Local path -> {self._path}"
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_config(
|
|
62
|
+
cls, dep_config: tp.Dict[str, tp.Any], work_dir: str
|
|
63
|
+
) -> "LocalPathDependency":
|
|
64
|
+
"""Create a dependency item from configuration."""
|
|
65
|
+
if "path" not in dep_config or "src" not in dep_config["path"]:
|
|
66
|
+
raise ValueError("Path not found in dependency configuration")
|
|
67
|
+
|
|
68
|
+
path = dep_config["path"]["src"]
|
|
69
|
+
img_dest = dep_config["dst"]
|
|
70
|
+
|
|
71
|
+
if not os.path.isabs(path):
|
|
72
|
+
path = os.path.join(work_dir, path)
|
|
73
|
+
|
|
74
|
+
return cls(path, img_dest)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class HttpDependency(base.AbstractDependency):
|
|
78
|
+
"""HTTP dependency item."""
|
|
79
|
+
|
|
80
|
+
CHUNK_SIZE = 8192
|
|
81
|
+
|
|
82
|
+
def __init__(self, endpoint: str, img_dest: str) -> None:
|
|
83
|
+
super().__init__()
|
|
84
|
+
self._endpoint = endpoint
|
|
85
|
+
self._img_dest = img_dest
|
|
86
|
+
self._local_path = None
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def img_dest(self) -> str | None:
|
|
90
|
+
"""Destination for the image."""
|
|
91
|
+
return self._img_dest
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def local_path(self) -> str | None:
|
|
95
|
+
"""Local path to the dependency."""
|
|
96
|
+
return self._local_path
|
|
97
|
+
|
|
98
|
+
def fetch(self, output_dir: str) -> None:
|
|
99
|
+
"""Fetch the dependency."""
|
|
100
|
+
filename = os.path.basename(self._endpoint)
|
|
101
|
+
output_path = os.path.join(output_dir, filename)
|
|
102
|
+
|
|
103
|
+
with bazooka.get(self._endpoint, stream=True) as r:
|
|
104
|
+
r.raise_for_status()
|
|
105
|
+
with open(output_path, "wb") as f:
|
|
106
|
+
for chunk in r.iter_content(chunk_size=self.CHUNK_SIZE):
|
|
107
|
+
f.write(chunk)
|
|
108
|
+
|
|
109
|
+
self._local_path = output_path
|
|
110
|
+
|
|
111
|
+
def __str__(self):
|
|
112
|
+
return f"URL -> {self._endpoint}"
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_config(
|
|
116
|
+
cls, dep_config: tp.Dict[str, tp.Any], work_dir: str
|
|
117
|
+
) -> "LocalPathDependency":
|
|
118
|
+
"""Create a dependency item from configuration."""
|
|
119
|
+
if "http" not in dep_config or "src" not in dep_config["http"]:
|
|
120
|
+
raise ValueError("URL not found in dependency configuration")
|
|
121
|
+
|
|
122
|
+
endpoint = dep_config["http"]["src"]
|
|
123
|
+
img_dest = dep_config["dst"]
|
|
124
|
+
|
|
125
|
+
return cls(endpoint, img_dest)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Copyright 2025 Genesis Corporation.
|
|
2
|
+
#
|
|
3
|
+
# All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
6
|
+
# not use this file except in compliance with the License. You may obtain
|
|
7
|
+
# a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
13
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
14
|
+
# License for the specific language governing permissions and limitations
|
|
15
|
+
# under the License.
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import typing as tp
|
|
21
|
+
import shutil
|
|
22
|
+
from importlib.resources import files
|
|
23
|
+
|
|
24
|
+
from genesis_devtools.builder import base
|
|
25
|
+
from genesis_devtools.logger import AbstractLogger, DummyLogger
|
|
26
|
+
from genesis_devtools import constants as c
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
file_provisioner_tmpl = """
|
|
30
|
+
provisioner "file" {{
|
|
31
|
+
source = "{source}"
|
|
32
|
+
destination = "{tmp_destination}"
|
|
33
|
+
}}
|
|
34
|
+
provisioner "shell" {{
|
|
35
|
+
inline = [
|
|
36
|
+
"sudo mv {tmp_destination} {destination}",
|
|
37
|
+
]
|
|
38
|
+
}}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
dev_keys_provisioner_tmpl = """
|
|
43
|
+
provisioner "file" {{
|
|
44
|
+
source = "{source}"
|
|
45
|
+
destination = "/tmp/__dev_keys"
|
|
46
|
+
}}
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
packer_build_tmpl = """
|
|
51
|
+
variable "output_directory" {{
|
|
52
|
+
type = string
|
|
53
|
+
default = "{output_directory}"
|
|
54
|
+
}}
|
|
55
|
+
|
|
56
|
+
build {{
|
|
57
|
+
source "qemu.{profile}" {{
|
|
58
|
+
name = "{name}"
|
|
59
|
+
}}
|
|
60
|
+
|
|
61
|
+
{file_provisioners}
|
|
62
|
+
|
|
63
|
+
provisioner "shell" {{
|
|
64
|
+
execute_command = "sudo -S env {{{{ .Vars }}}} {{{{ .Path }}}}"
|
|
65
|
+
script = "{script}"
|
|
66
|
+
env = {{
|
|
67
|
+
EXAMPLE_VARIABLE = "example_value"
|
|
68
|
+
}}
|
|
69
|
+
}}
|
|
70
|
+
|
|
71
|
+
{developer_keys}
|
|
72
|
+
}}
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PackerVariable(tp.NamedTuple):
|
|
77
|
+
name: str
|
|
78
|
+
value: str | int | float | list | dict | None = ""
|
|
79
|
+
var_tmpl: str = "{name} = {value}"
|
|
80
|
+
|
|
81
|
+
def render(self) -> str:
|
|
82
|
+
data = self._asdict()
|
|
83
|
+
data.pop("var_tmpl", None)
|
|
84
|
+
|
|
85
|
+
# Need quotes for strings in HCL
|
|
86
|
+
if isinstance(self.value, str):
|
|
87
|
+
data["value"] = f'"{self.value}"'
|
|
88
|
+
|
|
89
|
+
return self.var_tmpl.format(**data)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def variable_file_content(cls, overrides: tp.Dict[str, tp.Any]) -> str:
|
|
93
|
+
if not overrides:
|
|
94
|
+
return ""
|
|
95
|
+
|
|
96
|
+
variables = []
|
|
97
|
+
for k, v in overrides.items():
|
|
98
|
+
variables.append(cls(name=k, value=v))
|
|
99
|
+
|
|
100
|
+
return "\n".join([v.render() for v in variables])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _get_profile_files(base: str) -> str:
|
|
104
|
+
"""Get base files for the image."""
|
|
105
|
+
profile_files = []
|
|
106
|
+
for bfile in files(f"{c.PKG_NAME}.packer.{base}").iterdir():
|
|
107
|
+
profile_files.append(bfile)
|
|
108
|
+
|
|
109
|
+
return profile_files
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PackerBuilder(base.DummyImageBuilder):
|
|
113
|
+
"""Packer image builder.
|
|
114
|
+
|
|
115
|
+
Packer image builder that uses Packer tools to build images.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self, output_dir: str, logger: AbstractLogger | None = None
|
|
120
|
+
) -> None:
|
|
121
|
+
super().__init__()
|
|
122
|
+
self._logger = logger or DummyLogger()
|
|
123
|
+
self._output_dir = output_dir
|
|
124
|
+
|
|
125
|
+
def pre_build(
|
|
126
|
+
self,
|
|
127
|
+
image_dir: str,
|
|
128
|
+
image: base.Image,
|
|
129
|
+
deps: tp.List[base.AbstractDependency],
|
|
130
|
+
developer_keys: str | None = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Actions to prepare the environment for building the image."""
|
|
133
|
+
|
|
134
|
+
# Prepare the packer build file
|
|
135
|
+
# Data provisioners
|
|
136
|
+
provisioners = []
|
|
137
|
+
for i, dep in enumerate(deps):
|
|
138
|
+
tmp_dest = os.path.join(
|
|
139
|
+
"/tmp/", os.path.basename(dep.img_dest) + f"_{i}"
|
|
140
|
+
)
|
|
141
|
+
provisioners.append(
|
|
142
|
+
file_provisioner_tmpl.format(
|
|
143
|
+
source=dep.local_path,
|
|
144
|
+
destination=dep.img_dest,
|
|
145
|
+
tmp_destination=tmp_dest,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Prepare developer
|
|
150
|
+
if developer_keys:
|
|
151
|
+
dev_key_path = os.path.join(image_dir, "__dev_keys")
|
|
152
|
+
with open(dev_key_path, "w") as f:
|
|
153
|
+
f.write(developer_keys)
|
|
154
|
+
developer_keys_prov = dev_keys_provisioner_tmpl.format(
|
|
155
|
+
source=dev_key_path
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
developer_keys_prov = ""
|
|
159
|
+
|
|
160
|
+
profile = image.profile.replace("_", "-")
|
|
161
|
+
packer_build = packer_build_tmpl.format(
|
|
162
|
+
profile=profile,
|
|
163
|
+
name=image.name or profile,
|
|
164
|
+
file_provisioners="\n".join(provisioners),
|
|
165
|
+
script=image.script,
|
|
166
|
+
developer_keys=developer_keys_prov,
|
|
167
|
+
output_directory=self._output_dir,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Write the packer build file
|
|
171
|
+
main_path = os.path.join(image_dir, "main.pkr.hcl")
|
|
172
|
+
with open(main_path, "w") as f:
|
|
173
|
+
f.write(packer_build)
|
|
174
|
+
|
|
175
|
+
# Copy profile files to the image directory
|
|
176
|
+
profile_files = _get_profile_files(image.profile)
|
|
177
|
+
for bfile in profile_files:
|
|
178
|
+
shutil.copy(bfile, image_dir)
|
|
179
|
+
|
|
180
|
+
# Override variables if they are provided
|
|
181
|
+
if variables := PackerVariable.variable_file_content(
|
|
182
|
+
image.override or ()
|
|
183
|
+
):
|
|
184
|
+
with open(
|
|
185
|
+
os.path.join(image_dir, "overrides.auto.pkrvars.hcl"), "w"
|
|
186
|
+
) as f:
|
|
187
|
+
f.write(variables)
|
|
188
|
+
|
|
189
|
+
subprocess.run(["packer", "init", image_dir], check=True)
|
|
190
|
+
|
|
191
|
+
def build(
|
|
192
|
+
self,
|
|
193
|
+
image_dir: str,
|
|
194
|
+
image: base.Image,
|
|
195
|
+
developer_keys: str | None = None,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Actions to build the image."""
|
|
198
|
+
self._logger.important(f"Build image: {image.name}")
|
|
199
|
+
subprocess.run(
|
|
200
|
+
["packer", "build", "-parallel-builds=1", image_dir], check=True
|
|
201
|
+
)
|
|
File without changes
|