SnowSignal 0.1.1__tar.gz → 0.1.4.1__tar.gz

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.
Files changed (40) hide show
  1. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/.github/workflows/publish-to-test-pypi.yml +2 -2
  2. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/.gitlab/.gitlab-ci.yml +21 -7
  3. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/PKG-INFO +5 -4
  4. snowsignal-0.1.4.1/_version.py +34 -0
  5. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/docker-compose.dev.yml +4 -5
  6. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/docker-compose.local-dev.yml +4 -4
  7. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/docker-compose.yml +2 -2
  8. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/docs/local_dev.md +3 -0
  9. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/pyproject.toml +3 -2
  10. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/snowsignal/configure.py +7 -3
  11. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/snowsignal/dockerfile +3 -1
  12. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/snowsignal/netutils.py +1 -3
  13. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/snowsignal/packet.py +30 -0
  14. snowsignal-0.1.4.1/snowsignal/pva_packet.py +394 -0
  15. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/snowsignal/snowsignal.py +6 -2
  16. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/snowsignal/udp_relay_receive.py +43 -9
  17. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/snowsignal/udp_relay_transmit.py +112 -11
  18. snowsignal-0.1.4.1/tests/dockerfile +5 -0
  19. snowsignal-0.1.4.1/tests/integration/test_snowsignal.py +228 -0
  20. snowsignal-0.1.4.1/tests/testenv.sh +2 -0
  21. snowsignal-0.1.4.1/tests/unit/__init__.py +0 -0
  22. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/tests/unit/test_netutils.py +12 -2
  23. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/tests/unit/test_packet.py +50 -0
  24. snowsignal-0.1.4.1/tests/unit/test_pvapacket.py +254 -0
  25. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/tests/unit/test_udp_relay_receive.py +1 -0
  26. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/tests/unit/test_udp_relay_transmit.py +4 -3
  27. snowsignal-0.1.4.1/uv.lock +855 -0
  28. snowsignal-0.1.1/_version.py +0 -16
  29. snowsignal-0.1.1/tests/dockerfile +0 -5
  30. snowsignal-0.1.1/tests/unit/test_snowsignal.py +0 -102
  31. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/.gitignore +0 -0
  32. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/LICENSE +0 -0
  33. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/README.md +0 -0
  34. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/docs/local_dev_example.gif +0 -0
  35. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/docs/pvacess_communication_example.png +0 -0
  36. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/docs/socat_test_broadcast.md +0 -0
  37. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/docs/swarm_setup.md +0 -0
  38. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/snowsignal/__init__.py +0 -0
  39. {snowsignal-0.1.1 → snowsignal-0.1.4.1}/snowsignal/__main__.py +0 -0
  40. {snowsignal-0.1.1/tests/unit → snowsignal-0.1.4.1/tests/integration}/__init__.py +0 -0
@@ -28,7 +28,7 @@ jobs:
28
28
  strategy:
29
29
  matrix:
30
30
  os: [ubuntu-latest] #, mac-latest]
31
- python-version: ["3.11", "3.12"]
31
+ python-version: ["3.11", "3.12", "3.13"]
32
32
  runs-on: ${{ matrix.os }}
33
33
  continue-on-error: true
34
34
  steps:
@@ -45,7 +45,7 @@ jobs:
45
45
  - name: Run tests
46
46
  run: |
47
47
  PYTHON_PATH=$(which python)
48
- sudo $PYTHON_PATH -m coverage run --source=. -m unittest discover tests/
48
+ sudo -E $PYTHON_PATH -m coverage run --source=. -m unittest discover tests/
49
49
  - name: Gather coverage statistics
50
50
  if: ${{ always() }}
51
51
  run: |
@@ -1,7 +1,7 @@
1
1
  stages:
2
2
  - format-lint
3
3
  - test
4
- - pypi
4
+ - publish
5
5
  - build
6
6
  - deploy
7
7
 
@@ -26,7 +26,7 @@ format:
26
26
  image: "python:$VERSION"
27
27
  parallel:
28
28
  matrix:
29
- - VERSION: ['3.11', '3.12']
29
+ - VERSION: ['3.11', '3.12', '3.13']
30
30
 
31
31
  Run unittests:
32
32
  <<: *test_job_template
@@ -35,6 +35,8 @@ Run unittests:
35
35
  - coverage report -m
36
36
  - coverage xml
37
37
  coverage: '/TOTAL.*\s+(\d+\%)/'
38
+ variables:
39
+ FF_NETWORK_PER_BUILD: 1 # Integration tests need to send real UDP broadcasts so could potentially interfere with one another
38
40
  artifacts:
39
41
  when: always
40
42
  reports:
@@ -44,27 +46,39 @@ Run unittests:
44
46
 
45
47
  Publish:
46
48
  <<: *before_script_template
47
- stage: pypi
49
+ stage: publish
48
50
  when: on_success
49
51
  image: python:latest
52
+ variables:
53
+ GIT_DEPTH: 0
50
54
  script:
55
+ - export
51
56
  - pip install .[dist]
52
57
  - python -m build
53
- - TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/*
58
+ - HATCH_INDEX_AUTH=${CI_JOB_TOKEN} HATCH_INDEX_USER=gitlab-ci-token
59
+ python -m hatch publish -r ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/*
54
60
 
55
61
  Rebuild Image:
56
62
  stage: build
57
63
  image: docker
58
64
  variables:
59
65
  DOCKER_TAG: $CI_COMMIT_REF_NAME
66
+ PIPARGS:
67
+ rules:
68
+ - if: $CI_COMMIT_BRANCH == "dev"
69
+ variables:
70
+ PIPARGS: "--pre"
71
+ when: on_success
72
+ - if: $CI_COMMIT_BRANCH == "main"
73
+ when: on_success
74
+ - when: never
60
75
  services:
61
76
  - docker:dind
62
77
  script:
63
78
  - echo $NAME
64
- - docker build -t snowsignal snowsignal
65
79
  - docker login https://harbor.stfc.ac.uk -u $DOCKER_REG_NAME --password $DOCKER_REG_TOKEN
66
- - echo Build Identifiers - ${NAME}:$DOCKER_TAG
67
- - docker build -t harbor.stfc.ac.uk/isis-accelerator-controls/snowsignal:$DOCKER_TAG snowsignal
80
+ - echo "Build Identifiers - name=${NAME} tag=${DOCKER_TAG} pipargs=${PIPARGS}"
81
+ - docker build --no-cache --build-arg PIPARGS=${PIPARGS} -t harbor.stfc.ac.uk/isis-accelerator-controls/snowsignal:$DOCKER_TAG snowsignal
68
82
  - docker push harbor.stfc.ac.uk/isis-accelerator-controls/snowsignal:$DOCKER_TAG
69
83
 
70
84
  Deploy Development Image:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: SnowSignal
3
- Version: 0.1.1
3
+ Version: 0.1.4.1
4
4
  Summary: UDP Broadcast Relay
5
5
  Project-URL: Repository, https://github.com/ISISNeutronMuon/SnowSignal
6
6
  Author-email: Ivan Finch <ivan.finch@stfc.ac.uk>
@@ -16,14 +16,15 @@ Classifier: Programming Language :: Python
16
16
  Classifier: Topic :: System :: Networking
17
17
  Classifier: Typing :: Typed
18
18
  Requires-Python: >=3.11
19
+ Requires-Dist: cachetools>=5.5
19
20
  Requires-Dist: configargparse>=1.7
20
21
  Requires-Dist: psutil>=5.9
21
22
  Provides-Extra: dist
22
23
  Requires-Dist: build>=1.2; extra == 'dist'
23
- Requires-Dist: twine>=5.1; extra == 'dist'
24
+ Requires-Dist: hatch>=1.14; extra == 'dist'
24
25
  Provides-Extra: test
25
26
  Requires-Dist: coverage>=7.6; extra == 'test'
26
- Requires-Dist: ruff>0.6; extra == 'test'
27
+ Requires-Dist: ruff>0.9; extra == 'test'
27
28
  Requires-Dist: scapy~=2.0; extra == 'test'
28
29
  Description-Content-Type: text/markdown
29
30
 
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.4.1'
32
+ __version_tuple__ = version_tuple = (0, 1, 4, 1)
33
+
34
+ __commit_id__ = commit_id = None
@@ -1,22 +1,21 @@
1
- version: '3.9'
2
-
3
1
  services:
4
2
  snowsignal:
5
3
  image: harbor.stfc.ac.uk/isis-accelerator-controls/snowsignal:dev
6
4
  environment:
7
5
  SERVICENAME: '{{.Service.Name}}'
8
- LOGLEVEL: 'debug'
6
+ LOGLEVEL:
9
7
  networks:
10
- - snowsignal-test
8
+ - controls
11
9
  deploy:
12
10
  mode: global
13
11
  placement:
14
12
  max_replicas_per_node: 1
13
+ restart: unless-stopped
15
14
 
16
15
  probe:
17
16
  image: harbor.stfc.ac.uk/isis-accelerator-controls/internal-pvagw-tests
18
17
  networks:
19
- - snowsignal-test
18
+ - controls
20
19
  deploy:
21
20
  mode: global
22
21
  placement:
@@ -10,8 +10,8 @@ services:
10
10
  hostname: server1
11
11
  container_name: server1
12
12
  build:
13
- context: ./snowsignal
14
- command: "sleep infinity"
13
+ context: ./tests
14
+ command: "sh /snowsignal/tests/testenv.sh"
15
15
  volumes:
16
16
  - .:/snowsignal/
17
17
  networks:
@@ -22,8 +22,8 @@ services:
22
22
  hostname: server2
23
23
  container_name: server2
24
24
  build:
25
- context: ./snowsignal
26
- command: "sleep infinity"
25
+ context: ./tests
26
+ command: "sh /snowsignal/tests/testenv.sh"
27
27
  volumes:
28
28
  - .:/snowsignal/
29
29
  networks:
@@ -1,5 +1,3 @@
1
- version: '3.9'
2
-
3
1
  services:
4
2
  snowsignal:
5
3
  image: harbor.stfc.ac.uk/isis-accelerator-controls/snowsignal:main
@@ -11,6 +9,8 @@ services:
11
9
  mode: global
12
10
  placement:
13
11
  max_replicas_per_node: 1
12
+ restart: unless-stopped
13
+
14
14
 
15
15
  networks:
16
16
  controls:
@@ -25,6 +25,9 @@ socat - udp-datagram:255.255.255.255:5076,broadcast
25
25
  ```
26
26
  will allow a user to type text and press enter to send it as the payload of a UDP broadcast message.
27
27
 
28
+ > [!caution]
29
+ > 'socat' (and `nc`) do not produce valid UDP checksums in their broadcasts. If SnowSignal rebroadcasts a packet from one of these sources it may appear to have an invalid UDP checksum.
30
+
28
31
  `tcpdump` is used to show a raw dump of received network packets. This command line
29
32
  ```
30
33
  tcpdump -e -i eth0 udp and broadcast -vv -X
@@ -2,6 +2,7 @@
2
2
  name = "SnowSignal"
3
3
  dynamic = ["version"]
4
4
  dependencies = [
5
+ "cachetools>=5.5",
5
6
  "configargparse>=1.7",
6
7
  "psutil>=5.9",
7
8
  ]
@@ -33,12 +34,12 @@ Repository = "https://github.com/ISISNeutronMuon/SnowSignal"
33
34
  [project.optional-dependencies]
34
35
  test = [
35
36
  "scapy~=2.0",
36
- "ruff>0.6",
37
+ "ruff>0.9",
37
38
  "coverage>=7.6"
38
39
  ]
39
40
  dist = [
40
41
  "build>=1.2",
41
- "twine>=5.1",
42
+ "hatch>=1.14",
42
43
  ]
43
44
 
44
45
  [build-system]
@@ -20,12 +20,13 @@ class ConfigArgs(NamedTuple):
20
20
  mesh_port: int
21
21
  other_relays: list[str]
22
22
  log_level: str
23
+ decode_pvaccess: bool
23
24
 
24
25
 
25
26
  def configure(argv: Sequence[str] | None = None) -> ConfigArgs:
26
27
  """Setup configuration for the SnowSignal service"""
27
28
 
28
- p = configargparse.ArgParser()
29
+ p = configargparse.ArgParser(prog="snowsignal")
29
30
  # Remember to add new arguments to the Args class above!
30
31
  p.add_argument(
31
32
  "-t",
@@ -66,6 +67,7 @@ def configure(argv: Sequence[str] | None = None) -> ConfigArgs:
66
67
  default="info",
67
68
  help="Logging level",
68
69
  )
70
+ p.add_argument("--decode-pvaccess", action="store_true", help="Attempt to decode and log to INFO pvaccess messages")
69
71
  # Remember to add new arguments to the Args class above!
70
72
 
71
73
  # config = p.parse_args(argv)
@@ -82,10 +84,12 @@ def configure(argv: Sequence[str] | None = None) -> ConfigArgs:
82
84
  loglevel = logging.INFO
83
85
  case "debug":
84
86
  loglevel = logging.DEBUG
87
+ case _:
88
+ loglevel = logging.WARNING
85
89
 
86
90
  if loglevel < logging.INFO:
87
91
  logging.basicConfig(
88
- format="%(asctime)s - %(levelname)s - " "%(name)s.%(funcName)s: %(message)s",
92
+ format="%(asctime)s - %(levelname)s - %(name)s.%(funcName)s: %(message)s",
89
93
  encoding="utf-8",
90
94
  level=loglevel,
91
95
  )
@@ -100,7 +104,7 @@ def configure(argv: Sequence[str] | None = None) -> ConfigArgs:
100
104
  "Broadcast port (%i) and mesh port (%i) may not be the same", config.broadcast_port, config.mesh_port
101
105
  )
102
106
  raise ValueError(
103
- f"Broadcast port ({config.broadcast_port}) and " f"mesh port ({config.mesh_port}) may not be the same"
107
+ f"Broadcast port ({config.broadcast_port}) and mesh port ({config.mesh_port}) may not be the same"
104
108
  )
105
109
 
106
110
  return config
@@ -2,8 +2,10 @@
2
2
  # psutils requirement in requirements.txt
3
3
  FROM python:3.12-slim
4
4
 
5
+ ARG PIPARGS=
6
+
5
7
  # Token has read_api and read_repository permissions and so does not need to be kept secret
6
8
  # However it will expire
7
- RUN pip install SnowSignal --index-url https://gitlab.stfc.ac.uk/api/v4/projects/5671/packages/pypi/simple
9
+ RUN pip install SnowSignal ${PIPARGS} --no-cache-dir --index-url https://gitlab.stfc.ac.uk/api/v4/projects/5671/packages/pypi/simple
8
10
 
9
11
  CMD ["python", "-m", "snowsignal"]
@@ -65,9 +65,7 @@ def get_from_iface(
65
65
  if snicaddr.family == family:
66
66
  return getattr(snicaddr, attribute)
67
67
 
68
- raise ResourceNotFoundException(
69
- f"Could not identify the {family}, " "{attribute} associated with interface {iface}"
70
- )
68
+ raise ResourceNotFoundException(f"Could not identify the {family}, {attribute} associated with interface {iface}")
71
69
 
72
70
 
73
71
  def get_localipv4_from_iface(iface: str) -> str:
@@ -48,6 +48,10 @@ class Packet:
48
48
  ip_chksum: int | None = None
49
49
  ip_src_addr: str | None = None
50
50
  ip_dst_addr: str | None = None
51
+ ip_length: int | None = None
52
+ ipv4_identification: int | None = None
53
+ ipv4_more_fragments: bool = False
54
+ ipv4_fragmented_offset: int | None = None
51
55
 
52
56
  udp_src_port: int | None = None
53
57
  udp_dst_port: int | None = None
@@ -93,6 +97,14 @@ class Packet:
93
97
  if self.ip_version != 4:
94
98
  return
95
99
 
100
+ self.ip_length = iph[2]
101
+ self.ipv4_identification = iph[3]
102
+
103
+ # Decode fragment data
104
+ fragment_flag_and_offset = iph[4]
105
+ self.ipv4_more_fragments = bool(fragment_flag_and_offset & 0x2000)
106
+ self.ipv4_fragmented_offset = fragment_flag_and_offset & 0x1FFF
107
+
96
108
  # Calculate the length (of the header?)
97
109
  ihl = version_ihl & 0xF
98
110
  self._iph_length = ihl * 4
@@ -172,3 +184,21 @@ class Packet:
172
184
  def change_ethernet_source(self, newmac) -> None:
173
185
  """Change packet Ethernet source to a new MAC address"""
174
186
  self.raw = self.raw[0:6] + newmac + self.raw[12:]
187
+
188
+ def is_ipv4_fragmented(self) -> bool:
189
+ """Check if the IPv4 packet is fragmented"""
190
+ # Check for IP packet fragmentation, only IPv4 packets can be fragmented
191
+ # https://en.wikipedia.org/wiki/IPv4#Fragmentation_and_reassembly
192
+ # If a fragment then the More Fragments flag is set True for the first fragment and subsequent flags until the
193
+ # last segment in which the More Fragments flag is False. However, the Fragment Offset is only zero
194
+ # in the first fragment.
195
+ # We can identify a last fragment by the More Fragments flag being False and the Fragment Offset being non-zero.
196
+ # A non-fragmented IP packet will have the More Fragments flag set False and the Fragment Offset equal to zero.
197
+ logger.debug(
198
+ "ip_version = %i, ipv4_more_fragments = %s, ipv4_fragmented_offset = %i",
199
+ self.ip_version,
200
+ self.ipv4_more_fragments,
201
+ self.ipv4_fragmented_offset,
202
+ )
203
+
204
+ return self.ip_version == 4 and (self.ipv4_more_fragments or self.ipv4_fragmented_offset != 0)