beaker-kubevirt 1.0.0

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.
data/README.md ADDED
@@ -0,0 +1,274 @@
1
+ # Beaker::KubeVirt
2
+
3
+ A Beaker hypervisor provider for [KubeVirt](https://kubevirt.io), enabling automated acceptance testing of Puppet code using virtual machines running inside Kubernetes clusters.
4
+
5
+ ## Features
6
+
7
+ - Deploy VMs using KubeVirt's `VirtualMachine` objects
8
+ - Support for multiple image sources (PVC, ContainerDisk, DataVolume)
9
+ - Cloud-init configuration injection for user setup and SSH keys
10
+ - Multiple networking modes (port-forward, NodePort, Multus)
11
+ - Automatic VM lifecycle management (provision, test, cleanup)
12
+ - Integration with existing Beaker workflows
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'beaker-kubevirt'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ gem install beaker-kubevirt
32
+ ```
33
+
34
+ ## Configuration
35
+
36
+ ### Prerequisites
37
+
38
+ - A Kubernetes cluster with KubeVirt installed
39
+ - Valid kubeconfig file with cluster access
40
+ - SSH public key for VM access
41
+
42
+ ### Beaker Host Configuration
43
+
44
+ Configure your Beaker hosts file to use the KubeVirt hypervisor:
45
+
46
+ ```yaml
47
+ HOSTS:
48
+ centos-vm:
49
+ platform: el-8-x86_64
50
+ hypervisor: kubevirt
51
+ kubevirt_vm_image: docker://quay.io/kubevirt/fedora-cloud-container-disk-demo
52
+ kubevirt_network_mode: port-forward
53
+ kubevirt_ssh_key: ~/.ssh/id_rsa.pub
54
+ kubevirt_cpus: 2
55
+ kubevirt_memory: 4Gi
56
+
57
+ CONFIG:
58
+ # Global KubeVirt configuration
59
+ kubeconfig: <%= ENV.fetch('KUBECONFIG', '~/.kube/config') %>
60
+ kubecontext: my-context # optional
61
+ namespace: beaker-tests # required - namespace for all VMs
62
+ kubevirt_service_account: beaker-kubevirt-sa # optional - required for cross-namespace PVC cloning
63
+ ssh:
64
+ password: beaker
65
+ auth_methods: ['publickey', 'password']
66
+ ```
67
+
68
+ ### Configuration Options
69
+
70
+ | Option | Description | Required | Default | Location |
71
+ |--------|-------------|----------|---------|----------|
72
+ | `kubeconfig` | Path to kubeconfig file | Yes | `$KUBECONFIG` or `~/.kube/config` | CONFIG (global) |
73
+ | `kubecontext` | Kubernetes context to use | No | Current context | CONFIG (global) |
74
+ | `namespace` | Kubernetes namespace for VMs | **Yes** | `default` | **CONFIG (global)** |
75
+ | `kubevirt_service_account` | Service account for cross-namespace PVC cloning | No | `default` | CONFIG (global) |
76
+ | `kubevirt_vm_image` | VM image specification | Yes | - | HOSTS (per-host) |
77
+ | `kubevirt_network_mode` | Networking mode | No | `port-forward` | HOSTS (per-host) |
78
+ | `networks` | Custom network configuration | No | Auto-generated | HOSTS (per-host) |
79
+ | `kubevirt_ssh_key` | SSH public key path or content | Yes | Auto-detect from `~/.ssh/` | HOSTS (per-host) |
80
+ | `kubevirt_cpus` | CPU cores for VM | No | `1` | HOSTS (per-host) |
81
+ | `kubevirt_memory` | Memory for VM | No | `2Gi` | HOSTS (per-host) |
82
+ | `kubevirt_memory_overhead` | Extra memory added to the guest for the virt-launcher container memory limit. Raise this for Windows guests that OOMKill with the default. | No | `512Mi` | HOSTS (per-host), CONFIG |
83
+ | `kubevirt_memory_request` | Memory request for the VM pod. | No | Same as `kubevirt_memory` | HOSTS (per-host), CONFIG |
84
+ | `kubevirt_disk_size` | Size of the root disk | No | `10Gi` | HOSTS (per-host) |
85
+ | `kubevirt_cloud_init` | Custom cloud-init YAML | No | Auto-generated | HOSTS (per-host) |
86
+ | `kubevirt_vm_ssh_port` | SSH port inside the VM | No | `22` | HOSTS (per-host) |
87
+ | `kubevirt_readiness_probe_disabled` | Skip the VMI SSH readinessProbe. Default is `false` for pod-network modes and `true` for `multus` (where a probe from the virt-launcher netns typically can't reach a bridge-only guest). | No | mode-dependent | HOSTS (per-host), CONFIG |
88
+ | `kubevirt_readiness_probe` | Probe tuning hash (snake_case keys): `initial_delay_seconds` (30), `period_seconds` (10), `timeout_seconds` (3), `failure_threshold` (60), `success_threshold` (1). The default failure budget (600s) accommodates slow Windows first-boots. When the probe is enabled, `timeout` is auto-raised to cover this budget. | No | see description | HOSTS (per-host), CONFIG |
89
+ | `kubevirt_disable_virtio` | Disable virtio devices (for Windows compatibility). If set to true the disk bus will be set to `sata` and the network adapter will be model `e1000` | No | `false` | HOSTS (per-host) |
90
+
91
+ **Important**: The `namespace`, `kubeconfig`, and `kubecontext` options must be specified in the global `CONFIG` section, not per-host. All VMs will be created in the same Kubernetes namespace.
92
+
93
+ Notes:
94
+ - Several per-host options support global fallbacks via `CONFIG` (e.g., `kubevirt_cpus`, `kubevirt_memory`, `kubevirt_vm_ssh_port`).
95
+ - The `networks` key for Multus is intentionally unprefixed (use `networks`, not `kubevirt_networks`).
96
+ - Long-running commands aren't killed by idle-timeout drops, even when output streams in only one direction (e.g., a Windows scanner running for many minutes). The `port-forward` proxy no longer enforces a client-side read-silence timeout (it relied on net-ssh keepalive packets that net-ssh suppresses while server output is flowing) and instead sends a WebSocket protocol ping every 60s to keep the upstream tunnel alive. net-ssh keepalive defaults are also tightened to `keepalive_interval: 60`, `keepalive_maxcount: 5` — Beaker already enables `keepalive: true` — which protects `multus` and `nodeport` modes against NAT/conntrack timeouts. Override per-host under `ssh:` if needed.
97
+
98
+ ### VM Image Formats
99
+
100
+ The `kubevirt_vm_image` option supports several formats:
101
+
102
+ - **Container image**: `docker://quay.io/kubevirt/fedora-cloud-container-disk-demo` or `oci://quay.io/kubevirt/fedora-cloud-container-disk-demo`
103
+ - **PVC reference**: `pvc:my-vm-disk`, `my-vm-disk` (uses current namespace), or `namespace/pvc-name` (cross-namespace PVC)
104
+ - **DataVolume**: `http://example.com/my-datavolume.img` or `https://example.com/my-datavolume.img` NOTE: [KubeVirt CDI](https://github.com/kubevirt/containerized-data-importer) must be installed in the cluster for DataVolume support.
105
+
106
+ ### Cross-Namespace PVC Cloning
107
+
108
+ When cloning PVCs from a different namespace than where the VMs run, you must configure a service account with appropriate RBAC permissions. This is required because the DataVolume controller needs authorization to read PVCs in the source namespace.
109
+
110
+ #### Setup
111
+
112
+ 1. **Create the service account and RBAC resources:**
113
+
114
+ ```bash
115
+ kubectl create serviceaccount beaker-kubevirt -n beaker-tests
116
+ ```
117
+ Save the following RBAC configuration to `cluster-role.yaml`:
118
+
119
+ ```yaml
120
+ ---
121
+ apiVersion: rbac.authorization.k8s.io/v1
122
+ kind: ClusterRole
123
+ metadata:
124
+ name: beaker-kubevirt:volumes:clone
125
+ rules:
126
+ - apiGroups:
127
+ - cdi.kubevirt.io
128
+ resources:
129
+ - datavolumes/source
130
+ verbs:
131
+ - '*'
132
+ ```
133
+
134
+ Then apply it:
135
+
136
+ ```bash
137
+ kubectl apply -f cluster-role.yaml
138
+ ```
139
+
140
+ Save the following RoleBinding configuration to `pvc-clone-rbac.yaml`:
141
+
142
+ ```yaml
143
+ ---
144
+ apiVersion: rbac.authorization.k8s.io/v1
145
+ kind: RoleBinding
146
+ metadata:
147
+ name: beaker-kubevirt:volumes:clone-binding
148
+ namespace: source-namespace
149
+ roleRef:
150
+ apiGroup: rbac.authorization.k8s.io
151
+ kind: ClusterRole
152
+ name: beaker-kubevirt:volumes:clone
153
+ subjects:
154
+ - kind: ServiceAccount
155
+ name: beaker-kubevirt
156
+ namespace: destination-namespace
157
+ ```
158
+
159
+ Apply the RoleBinding (replace `source-namespace` and `destination-namespace` with your actual namespaces):
160
+
161
+ ```bash
162
+ kubectl apply -f pvc-clone-rbac.yaml
163
+ ```
164
+
165
+ This creates a `beaker-kubevirt` service account in the beaker test namespace with permissions to update DataVolumes to indicate a clone source in the specified source namespace.
166
+
167
+ 2. **Configure beaker to use the service account:**
168
+
169
+ ```yaml
170
+ CONFIG:
171
+ namespace: beaker-tests
172
+ kubevirt_service_account: beaker-kubevirt
173
+ ```
174
+
175
+ 3. **Reference PVCs from other namespaces:**
176
+
177
+ ```yaml
178
+ HOSTS:
179
+ test-vm:
180
+ kubevirt_vm_image: other-namespace/source-pvc-name
181
+ ```
182
+
183
+ This configuration allows Beaker to create VMs that clone PVCs from different namespaces with the minimum required permissions.
184
+
185
+ ### Network Modes
186
+
187
+ - **port-forward**: Uses `kubectl port-forward` (default, works everywhere)
188
+ - **nodeport**: Creates a NodePort service (requires node access)
189
+ - **multus**: Uses Multus bridge networking (requires Multus CNI)
190
+
191
+ #### Multus networks example
192
+
193
+ When using `kubevirt_network_mode: multus`, specify one or more Multus attachments via an unprefixed `networks:` array. Each item requires a unique `name` and the Multus `networkName` provided as `multus_network_name`.
194
+
195
+ ```yaml
196
+ HOSTS:
197
+ multus-vm:
198
+ platform: el-8-x86_64
199
+ hypervisor: kubevirt
200
+ kubevirt_vm_image: docker://quay.io/kubevirt/fedora-cloud-container-disk-demo
201
+ kubevirt_network_mode: multus
202
+ kubevirt_ssh_key: ~/.ssh/id_rsa.pub
203
+ networks:
204
+ - name: ext0
205
+ multus_network_name: my-bridge-network
206
+ - name: ext1
207
+ multus_network_name: another-network
208
+ ```
209
+
210
+ ## Usage Example
211
+
212
+ ```yaml
213
+ # beaker-hosts.yaml
214
+ HOSTS:
215
+ puppet-agent:
216
+ platform: el-8-x86_64
217
+ hypervisor: kubevirt
218
+ kubevirt_vm_image: docker://quay.io/kubevirt/centos-stream8-container-disk-demo
219
+ kubevirt_network_mode: port-forward
220
+ kubevirt_ssh_key: ~/.ssh/id_rsa.pub
221
+ kubevirt_cpus: 2
222
+ kubevirt_memory: 4Gi
223
+ CONFIG:
224
+ # Global KubeVirt configuration
225
+ kubeconfig: ~/.kube/config
226
+ namespace: beaker-tests
227
+ ssh:
228
+ auth_methods: ['publickey']
229
+ ```
230
+
231
+ ```ruby
232
+ # spec/acceptance/basic_spec.rb
233
+ require 'spec_helper_acceptance'
234
+
235
+ describe 'basic functionality' do
236
+ context 'on KubeVirt VM' do
237
+ it 'should provision successfully' do
238
+ expect(fact_on(default, 'kernel')).to eq('Linux')
239
+ end
240
+
241
+ it 'should have SSH access' do
242
+ result = on(default, 'echo "Hello from KubeVirt VM"')
243
+ expect(result.stdout.strip).to eq('Hello from KubeVirt VM')
244
+ end
245
+ end
246
+ end
247
+ ```
248
+
249
+ ## Labels and Cleanup
250
+
251
+ All resources created are labeled for traceability and cleanup:
252
+ - `beaker/test-group`: Identifies the run (e.g., `beaker-<hex>`)
253
+ - `beaker/host`: Host name from your Beaker inventory
254
+
255
+ These labels are used during `cleanup` to remove VMs, secrets, and services associated with the test group.
256
+
257
+ ## Requirements
258
+
259
+ - KubeVirt and Kubernetes cluster access via `kubeconfig`
260
+ - For `port-forward` networking mode: `kubectl` access to the cluster nodes and permission to port-forward
261
+
262
+ ## Development
263
+
264
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
265
+
266
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
267
+
268
+ ## Contributing
269
+
270
+ Bug reports and pull requests are welcome on GitHub at https://github.com/voxpupuli/beaker-kubevirt.
271
+
272
+ ## License
273
+
274
+ The gem is available as open source under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ begin
9
+ require 'rubocop/rake_task'
10
+ RuboCop::RakeTask.new
11
+ task default: %i[spec rubocop]
12
+ rescue LoadError
13
+ # RuboCop is optional
14
+ task default: [:spec] # rubocop:disable Rake/DuplicateTask
15
+ end
16
+
17
+ desc 'Run acceptance tests (requires KubeVirt cluster)'
18
+ task :acceptance do
19
+ puts 'Running acceptance tests...'
20
+ puts 'Note: This requires a KubeVirt-enabled Kubernetes cluster'
21
+ # Add acceptance test commands here
22
+ end
23
+
24
+ desc 'Run examples'
25
+ task :examples do
26
+ ruby 'examples/usage.rb'
27
+ end
28
+
29
+ begin
30
+ require 'github_changelog_generator/task'
31
+
32
+ GitHubChangelogGenerator::RakeTask.new :changelog do |config|
33
+ config.header = "# Changelog\n\nAll notable changes to this project will be documented in this file."
34
+ config.exclude_labels = %w[duplicate question invalid wontfix wont-fix skip-changelog github_actions]
35
+ config.user = 'voxpupuli'
36
+ config.project = 'beaker-kubevirt'
37
+ config.future_release = Gem::Specification.load("#{config.project}.gemspec").version
38
+ end
39
+ rescue LoadError
40
+ # Optional group in bundler
41
+ end
@@ -0,0 +1,57 @@
1
+ # Example cloud-init configuration for KubeVirt VMs
2
+ # This is automatically generated by beaker-kubevirt, but can be customized
3
+
4
+ #cloud-config
5
+
6
+ # User configuration
7
+ users:
8
+ - name: beaker
9
+ sudo: ALL=(ALL) NOPASSWD:ALL
10
+ ssh_authorized_keys:
11
+ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ... # Your SSH public key here
12
+ shell: /bin/bash
13
+ lock_passwd: false
14
+
15
+ # Hostname configuration
16
+ hostname: ${VM_NAME}
17
+ fqdn: ${VM_NAME}.beaker.local
18
+
19
+ # SSH configuration
20
+ ssh_pwauth: false
21
+ disable_root: false
22
+
23
+ # Network configuration (optional)
24
+ # network:
25
+ # version: 2
26
+ # ethernets:
27
+ # eth0:
28
+ # dhcp4: true
29
+
30
+ # Package installation (optional)
31
+ # packages:
32
+ # - curl
33
+ # - wget
34
+ # - git
35
+
36
+ # Commands to run on first boot (optional)
37
+ # runcmd:
38
+ # - echo "VM is ready" > /tmp/vm-ready
39
+
40
+ # Write files (optional)
41
+ # write_files:
42
+ # - path: /etc/motd
43
+ # content: |
44
+ # Welcome to Beaker KubeVirt VM
45
+ # This VM was provisioned for acceptance testing
46
+ # permissions: '0644'
47
+
48
+ # Set timezone (optional)
49
+ # timezone: UTC
50
+
51
+ # Configure logging
52
+ # rsyslog:
53
+ # remotes:
54
+ # beaker: "192.168.1.100:514"
55
+
56
+ # Final message
57
+ final_message: "Beaker KubeVirt VM is ready for testing"
@@ -0,0 +1,91 @@
1
+ ---
2
+ HOSTS:
3
+ centos-8-kubevirt:
4
+ platform: el-8-x86_64
5
+ hypervisor: kubevirt
6
+ # VM-specific configuration
7
+ kubevirt_vm_image: docker://quay.io/kubevirt/centos-stream8-container-disk-demo
8
+ kubevirt_network_mode: port-forward
9
+ kubevirt_ssh_key: ~/.ssh/id_rsa.pub
10
+ kubevirt_cpus: 2
11
+ kubevirt_memory: 4Gi
12
+ # Extra memory added to the guest for the virt-launcher container memory
13
+ # limit. Bump this for Windows guests that get OOMKilled with the default.
14
+ kubevirt_memory_overhead: 512Mi
15
+ # kubevirt_memory_request: 4Gi # defaults to kubevirt_memory
16
+ # Optional: tune the SSH readinessProbe (KubeVirt runs this from virt-launcher,
17
+ # flips the VMI Ready condition to True once sshd is accepting connections).
18
+ # kubevirt_readiness_probe:
19
+ # initial_delay_seconds: 30
20
+ # period_seconds: 10
21
+ # failure_threshold: 120 # 120 * 10s = 20 min, for very slow Windows first-boots
22
+ # Beaker-specific configuration
23
+ user: centos
24
+ ssh:
25
+ auth_methods:
26
+ - publickey
27
+
28
+ ubuntu-20-kubevirt:
29
+ platform: ubuntu-20.04-x86_64
30
+ hypervisor: kubevirt
31
+ # Use a PVC as the VM image
32
+ kubevirt_vm_image: pvc:ubuntu-20-04-cloud-image
33
+ kubevirt_network_mode: nodeport
34
+ kubevirt_ssh_key: ~/.ssh/id_rsa.pub
35
+ kubevirt_cpus: 1
36
+ kubevirt_memory: 2Gi
37
+ user: ubuntu
38
+ ssh:
39
+ auth_methods:
40
+ - publickey
41
+
42
+ debian-12-kubevirt:
43
+ platform: debian-12-x86_64
44
+ hypervisor: kubevirt
45
+ # Use a PVC from a different namespace
46
+ kubevirt_vm_image: images/debian-12-cloud-image
47
+ kubevirt_network_mode: port-forward
48
+ kubevirt_ssh_key: ~/.ssh/id_rsa.pub
49
+ kubevirt_cpus: 1
50
+ kubevirt_memory: 2Gi
51
+ user: debian
52
+ ssh:
53
+ auth_methods:
54
+ - publickey
55
+
56
+ fedora-35-kubevirt:
57
+ platform: fedora-35-x86_64
58
+ hypervisor: kubevirt
59
+ # Use Multus external networking
60
+ kubevirt_vm_image: docker://quay.io/kubevirt/fedora-cloud-container-disk-demo
61
+ kubevirt_network_mode: multus
62
+ kubevirt_ssh_key: ~/.ssh/id_rsa.pub
63
+ kubevirt_cpus: 2
64
+ kubevirt_memory: 2Gi
65
+ networks:
66
+ - name: ext0
67
+ multus_network_name: my-bridge-network
68
+ user: fedora
69
+ ssh:
70
+ auth_methods:
71
+ - publickey
72
+
73
+ CONFIG:
74
+ # Global KubeVirt configuration, use the KUBECONFIG env var or default path
75
+ kubeconfig: <%= ENV.fetch('KUBECONFIG', '~/.kube/config') %>
76
+ kubecontext: my-cluster # optional
77
+ namespace: beaker-tests # required - namespace for all VMs
78
+
79
+ # Global SSH configuration
80
+ ssh:
81
+ password: beaker
82
+ auth_methods:
83
+ - publickey
84
+ - password
85
+
86
+ # Global timeout settings
87
+ timeout: 300
88
+
89
+ # Test configuration
90
+ log_level: verbose
91
+ color: true
data/examples/usage.rb ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example usage of beaker-kubevirt
5
+ # This demonstrates how to configure and use the KubeVirt hypervisor
6
+
7
+ require 'beaker/kubevirt'
8
+
9
+ # Example Beaker host configuration
10
+ hosts_config = {
11
+ 'HOSTS' => {
12
+ 'centos-vm' => {
13
+ 'platform' => 'el-8-x86_64',
14
+ 'hypervisor' => 'kubevirt',
15
+ 'kubevirt_vm_image' => 'docker://quay.io/kubevirt/centos-stream8-container-disk-demo',
16
+ 'kubevirt_network_mode' => 'port-forward',
17
+ 'kubevirt_ssh_key' => '~/.ssh/id_rsa.pub',
18
+ 'kubevirt_cpus' => 2,
19
+ 'kubevirt_memory' => '4Gi',
20
+ },
21
+ 'ubuntu-vm' => {
22
+ 'platform' => 'ubuntu-20.04-x86_64',
23
+ 'hypervisor' => 'kubevirt',
24
+ 'kubevirt_vm_image' => 'pvc:ubuntu-20-04-disk',
25
+ 'kubevirt_network_mode' => 'nodeport',
26
+ 'kubevirt_ssh_key' => '~/.ssh/other_key.pub',
27
+ 'kubevirt_cpus' => 1,
28
+ 'kubevirt_memory' => '2Gi',
29
+ },
30
+ },
31
+ 'CONFIG' => {
32
+ # Global KubeVirt configuration
33
+ 'kubeconfig' => '~/.kube/config',
34
+ 'namespace' => 'beaker-tests', # required global setting
35
+ 'ssh' => {
36
+ 'auth_methods' => ['publickey'],
37
+ },
38
+ },
39
+ }
40
+
41
+ puts 'Example KubeVirt Beaker configuration:'
42
+ puts hosts_config.to_yaml
43
+
44
+ # Example of programmatic usage
45
+ if $PROGRAM_NAME == __FILE__
46
+ # This would typically be called by Beaker
47
+ options = {
48
+ logger: Logger.new($stdout),
49
+ kubeconfig: ENV['KUBECONFIG'] || File.expand_path('~/.kube/config'),
50
+ namespace: 'beaker-tests',
51
+ kubevirt_vm_image: 'docker://quay.io/kubevirt/fedora-cloud-container-disk-demo',
52
+ kubevirt_network_mode: 'port-forward',
53
+ kubevirt_ssh_key: File.expand_path('~/.ssh/id_rsa.pub'),
54
+ }
55
+
56
+ hosts = [
57
+ {
58
+ 'name' => 'test-vm',
59
+ 'platform' => 'el-8-x86_64',
60
+ },
61
+ ]
62
+
63
+ begin
64
+ hypervisor = Beaker::KubeVirt.new(hosts, options)
65
+ puts 'Successfully created KubeVirt hypervisor instance'
66
+ puts "Test group identifier: #{hypervisor.instance_variable_get(:@test_group_identifier)}"
67
+ rescue StandardError => e
68
+ puts "Error creating hypervisor: #{e.message}"
69
+ end
70
+ end